clementine-agent 1.4.2 → 1.4.3

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.
@@ -2240,6 +2240,140 @@ export async function cmdDashboard(opts) {
2240
2240
  writeFileSync(queueFile, JSON.stringify(queue, null, 2));
2241
2241
  res.json({ ok: true, id });
2242
2242
  });
2243
+ app.get('/api/vault-files', async (req, res) => {
2244
+ try {
2245
+ const limit = Math.min(parseInt(String(req.query.limit ?? '120'), 10) || 120, 500);
2246
+ const sinceDays = Math.max(parseInt(String(req.query.sinceDays ?? '30'), 10) || 30, 1);
2247
+ const agentFilter = typeof req.query.agent === 'string' ? req.query.agent : '';
2248
+ const folderFilter = typeof req.query.folder === 'string' ? req.query.folder : '';
2249
+ const search = typeof req.query.q === 'string' ? req.query.q.toLowerCase() : '';
2250
+ const includeAuto = req.query.includeAuto === '1';
2251
+ const cutoffMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
2252
+ const vaultRoot = path.join(BASE_DIR, 'vault');
2253
+ const matter = (await import('gray-matter')).default;
2254
+ const files = [];
2255
+ function walk(dir) {
2256
+ let entries = [];
2257
+ try {
2258
+ entries = readdirSync(dir);
2259
+ }
2260
+ catch {
2261
+ return;
2262
+ }
2263
+ for (const e of entries) {
2264
+ if (e.startsWith('.'))
2265
+ continue;
2266
+ const full = path.join(dir, e);
2267
+ let stat;
2268
+ try {
2269
+ stat = statSync(full);
2270
+ }
2271
+ catch {
2272
+ continue;
2273
+ }
2274
+ if (stat.isDirectory()) {
2275
+ walk(full);
2276
+ continue;
2277
+ }
2278
+ if (!e.endsWith('.md'))
2279
+ continue;
2280
+ if (e.endsWith('.md.bak'))
2281
+ continue;
2282
+ if (stat.mtimeMs < cutoffMs)
2283
+ continue;
2284
+ const rel = path.relative(vaultRoot, full);
2285
+ // Skip auto-generated MCP/skill wrappers unless explicitly requested.
2286
+ // Path patterns: 00-System/skills/auto/* (generated tool wrappers).
2287
+ if (!includeAuto && rel.startsWith('00-System/skills/auto/'))
2288
+ continue;
2289
+ if (!includeAuto && rel.startsWith('00-System/agents/') && /\/(MEMORY|HEARTBEAT|TASKS|CRON)\.md$/.test(rel))
2290
+ continue;
2291
+ const folder = path.dirname(rel).split(path.sep)[0] || '';
2292
+ let agentSlug = null;
2293
+ if (rel.startsWith('00-System/agents/')) {
2294
+ const m = rel.match(/^00-System\/agents\/([^/]+)\//);
2295
+ if (m)
2296
+ agentSlug = m[1];
2297
+ }
2298
+ // Skip system housekeeping files (their author will surface via mtime in agent's own dir)
2299
+ let title = path.basename(rel, '.md');
2300
+ let typeTag = null;
2301
+ try {
2302
+ const head = readFileSync(full, 'utf-8').slice(0, 4000);
2303
+ const parsed = matter(head);
2304
+ const data = parsed.data;
2305
+ if (typeof data.title === 'string')
2306
+ title = data.title;
2307
+ else if (typeof data.name === 'string')
2308
+ title = data.name;
2309
+ else {
2310
+ const h1 = (parsed.content || '').match(/^#\s+(.+)$/m);
2311
+ if (h1)
2312
+ title = h1[1].trim();
2313
+ }
2314
+ if (typeof data.type === 'string')
2315
+ typeTag = data.type;
2316
+ }
2317
+ catch { /* */ }
2318
+ files.push({
2319
+ path: full,
2320
+ relPath: rel,
2321
+ title,
2322
+ folder,
2323
+ agentSlug,
2324
+ mtime: new Date(stat.mtimeMs).toISOString(),
2325
+ sizeBytes: stat.size,
2326
+ type: typeTag,
2327
+ });
2328
+ }
2329
+ }
2330
+ walk(vaultRoot);
2331
+ let filtered = files
2332
+ .sort((a, b) => b.mtime.localeCompare(a.mtime))
2333
+ .filter(f => {
2334
+ if (agentFilter === '__shared__' && f.agentSlug != null)
2335
+ return false;
2336
+ if (agentFilter && agentFilter !== '__shared__' && f.agentSlug !== agentFilter)
2337
+ return false;
2338
+ if (folderFilter && f.folder !== folderFilter)
2339
+ return false;
2340
+ if (search) {
2341
+ const hay = (f.title + ' ' + f.relPath).toLowerCase();
2342
+ if (!hay.includes(search))
2343
+ return false;
2344
+ }
2345
+ return true;
2346
+ })
2347
+ .slice(0, limit);
2348
+ // Compute folder counts for filter chips
2349
+ const folderCounts = {};
2350
+ for (const f of files)
2351
+ folderCounts[f.folder] = (folderCounts[f.folder] || 0) + 1;
2352
+ res.json({ files: filtered, total: files.length, folderCounts });
2353
+ }
2354
+ catch (err) {
2355
+ res.status(500).json({ error: String(err) });
2356
+ }
2357
+ });
2358
+ app.get('/api/vault-file', async (req, res) => {
2359
+ try {
2360
+ const relPath = typeof req.query.path === 'string' ? req.query.path : '';
2361
+ if (!relPath || relPath.includes('..')) {
2362
+ res.status(400).json({ error: 'Bad path' });
2363
+ return;
2364
+ }
2365
+ const full = path.join(BASE_DIR, 'vault', relPath);
2366
+ if (!existsSync(full)) {
2367
+ res.status(404).json({ error: 'Not found' });
2368
+ return;
2369
+ }
2370
+ const content = readFileSync(full, 'utf-8');
2371
+ res.json({ path: relPath, content });
2372
+ }
2373
+ catch (err) {
2374
+ res.status(500).json({ error: String(err) });
2375
+ }
2376
+ });
2243
2377
  app.get('/api/memory', async (_req, res) => {
2244
2378
  res.json(await getMemory());
2245
2379
  });
@@ -10610,6 +10744,26 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
10610
10744
  /* Hide chat profile selector when default — the row gets cleaner */
10611
10745
  .home-chat-input-row .chat-profile-spacer { display: none; }
10612
10746
 
10747
+ /* Vault file folder chips */
10748
+ .vault-folder-chip {
10749
+ padding: 4px 12px;
10750
+ border: 1px solid var(--border);
10751
+ border-radius: 16px;
10752
+ font-size: var(--text-sm);
10753
+ color: var(--text-secondary);
10754
+ cursor: pointer;
10755
+ transition: all var(--motion);
10756
+ background: var(--bg-secondary);
10757
+ user-select: none;
10758
+ }
10759
+ .vault-folder-chip:hover { background: var(--bg-hover); color: var(--text-primary); }
10760
+ .vault-folder-chip.active {
10761
+ background: var(--clementine-bg);
10762
+ color: var(--clementine);
10763
+ border-color: var(--clementine);
10764
+ font-weight: 500;
10765
+ }
10766
+
10613
10767
  /* ── Task Cards ─────────────────────────── */
10614
10768
  .task-grid {
10615
10769
  display: grid;
@@ -11765,6 +11919,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
11765
11919
  <div class="tab-bar" id="intelligence-tabs" style="margin:0 0 0 18px">
11766
11920
  <button class="active" data-icon="database" onclick="switchTab('intelligence','search')"><span class="icon-slot"></span> Memory</button>
11767
11921
  <button data-icon="sparkles" onclick="switchTab('intelligence','graph')"><span class="icon-slot"></span> Knowledge</button>
11922
+ <button data-icon="fileText" onclick="switchTab('intelligence','files')"><span class="icon-slot"></span> Files</button>
11768
11923
  <button data-icon="folder" onclick="switchTab('intelligence','sources')"><span class="icon-slot"></span> Ingestion</button>
11769
11924
  <button data-icon="zap" onclick="switchTab('intelligence','health')"><span class="icon-slot"></span> Health <span class="tab-badge" id="brain-health-badge" style="display:none;background:#ef4444;color:#fff">0</span></button>
11770
11925
  <button data-icon="users" onclick="switchTab('intelligence','user-model')"><span class="icon-slot"></span> User Model</button>
@@ -11973,6 +12128,26 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
11973
12128
  <div class="tab-pane" id="tab-intelligence-runs">
11974
12129
  <div id="brain-runs-list"></div>
11975
12130
  </div>
12131
+ <div class="tab-pane" id="tab-intelligence-files">
12132
+ <div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;flex-wrap:wrap">
12133
+ <input type="text" id="vault-files-search" placeholder="Search title or path..." style="flex:1;min-width:200px;padding:7px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-input);color:var(--text-primary);font-size:13px" oninput="refreshVaultFiles()">
12134
+ <select id="vault-files-agent-filter" onchange="refreshVaultFiles()" style="padding:7px 10px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-secondary);color:var(--text-primary);font-size:12px">
12135
+ <option value="">All authors</option>
12136
+ <option value="__shared__">Shared (vault root)</option>
12137
+ </select>
12138
+ <select id="vault-files-since" onchange="refreshVaultFiles()" style="padding:7px 10px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-secondary);color:var(--text-primary);font-size:12px">
12139
+ <option value="7">Past 7 days</option>
12140
+ <option value="30" selected>Past 30 days</option>
12141
+ <option value="90">Past 90 days</option>
12142
+ <option value="365">Past year</option>
12143
+ </select>
12144
+ <button class="btn-sm" onclick="refreshVaultFiles()">Refresh</button>
12145
+ </div>
12146
+ <div id="vault-files-folder-chips" style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px"></div>
12147
+ <div id="vault-files-list">
12148
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>
12149
+ </div>
12150
+ </div>
11976
12151
  <div class="tab-pane" id="tab-intelligence-health">
11977
12152
  <div style="display:flex;align-items:center;gap:8px;margin-bottom:14px;flex-wrap:wrap">
11978
12153
  <button class="btn-sm" onclick="memoryHealthAction('janitor')" title="Run the janitor cleanup pass now">Run cleanup</button>
@@ -14565,6 +14740,7 @@ function switchTab(group, tab) {
14565
14740
  if (group === 'intelligence') {
14566
14741
  if (tab === 'graph') refreshGraph();
14567
14742
  if (tab === 'memory') refreshMemory();
14743
+ if (tab === 'files' && typeof refreshVaultFiles === 'function') refreshVaultFiles();
14568
14744
  if (tab === 'health') {
14569
14745
  if (typeof refreshMemoryHealth === 'function') refreshMemoryHealth();
14570
14746
  if (typeof refreshClaims === 'function') refreshClaims();
@@ -19816,6 +19992,134 @@ async function memoryHealthAction(action) {
19816
19992
  }
19817
19993
  }
19818
19994
 
19995
+ // ── Vault Files (Brain → Files tab) ──────────────────────────────
19996
+ var _vaultFilesCache = null;
19997
+ var _vaultFilesFolder = ''; // current folder filter
19998
+
19999
+ async function refreshVaultFiles() {
20000
+ var listEl = document.getElementById('vault-files-list');
20001
+ if (!listEl) return;
20002
+ var q = document.getElementById('vault-files-search')?.value || '';
20003
+ var agent = document.getElementById('vault-files-agent-filter')?.value || '';
20004
+ var since = document.getElementById('vault-files-since')?.value || '30';
20005
+ // Show skeleton while loading
20006
+ listEl.innerHTML = '<div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>';
20007
+ try {
20008
+ var url = '/api/vault-files?sinceDays=' + encodeURIComponent(since)
20009
+ + (q ? '&q=' + encodeURIComponent(q) : '')
20010
+ + (agent ? '&agent=' + encodeURIComponent(agent) : '')
20011
+ + (_vaultFilesFolder ? '&folder=' + encodeURIComponent(_vaultFilesFolder) : '');
20012
+ var r = await apiFetch(url);
20013
+ var d = await r.json();
20014
+ var files = d.files || [];
20015
+ _vaultFilesCache = files;
20016
+ // Populate agent filter from ALL files response (use server's full set, not filtered)
20017
+ var agentSel = document.getElementById('vault-files-agent-filter');
20018
+ if (agentSel && agentSel.options.length <= 2) {
20019
+ var slugs = [...new Set(files.map(function(f) { return f.agentSlug; }).filter(Boolean))].sort();
20020
+ slugs.forEach(function(slug) {
20021
+ var opt = document.createElement('option');
20022
+ opt.value = slug;
20023
+ opt.textContent = slug;
20024
+ agentSel.appendChild(opt);
20025
+ });
20026
+ }
20027
+ // Render folder filter chips (using folderCounts from server)
20028
+ var chipsEl = document.getElementById('vault-files-folder-chips');
20029
+ if (chipsEl && d.folderCounts) {
20030
+ var folders = Object.entries(d.folderCounts).sort(function(a, b) { return b[1] - a[1]; });
20031
+ var totalCount = folders.reduce(function(s, p) { return s + p[1]; }, 0);
20032
+ var chipHtml = '<div class="vault-folder-chip' + (_vaultFilesFolder === '' ? ' active' : '') + '" data-folder="" onclick="setVaultFolderFilter(\\x27\\x27)">All <span style="opacity:0.6">' + totalCount + '</span></div>';
20033
+ folders.forEach(function(p) {
20034
+ var folder = p[0]; var count = p[1];
20035
+ if (!folder) return;
20036
+ chipHtml += '<div class="vault-folder-chip' + (_vaultFilesFolder === folder ? ' active' : '') + '" data-folder="' + esc(folder) + '" onclick="setVaultFolderFilter(\\x27' + esc(folder) + '\\x27)">' + esc(folder) + ' <span style="opacity:0.6">' + count + '</span></div>';
20037
+ });
20038
+ chipsEl.innerHTML = chipHtml;
20039
+ }
20040
+ if (files.length === 0) {
20041
+ listEl.innerHTML = '<div class="empty-cta"><div class="label">No recent files</div><div class="hint">Try a wider time window or different filter.</div></div>';
20042
+ return;
20043
+ }
20044
+ var html = '<div style="font-size:11px;color:var(--text-muted);margin-bottom:10px">Showing ' + files.length + ' of ' + d.total + ' files modified in the last ' + since + ' days.</div>';
20045
+ html += '<div style="display:flex;flex-direction:column;gap:1px;border:1px solid var(--border);border-radius:var(--radius-md);overflow:hidden;background:var(--bg-card)">';
20046
+ for (var i = 0; i < files.length; i++) {
20047
+ var f = files[i];
20048
+ var agentBadge = f.agentSlug
20049
+ ? '<span style="font-size:10px;background:var(--clementine-bg);color:var(--clementine);padding:2px 7px;border-radius:var(--radius-xs);font-weight:500">' + esc(f.agentSlug) + '</span>'
20050
+ : '<span style="font-size:10px;background:var(--bg-tertiary);color:var(--text-muted);padding:2px 7px;border-radius:var(--radius-xs)">shared</span>';
20051
+ var typeBadge = f.type ? '<span style="font-size:10px;color:var(--text-muted);margin-right:6px">' + esc(f.type) + '</span>' : '';
20052
+ html += '<div class="vault-file-row clickable-row" data-path="' + esc(f.relPath) + '" style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--bg-secondary);border-bottom:1px solid var(--border-light);font-size:13px">'
20053
+ + '<div style="flex:1;min-width:0">'
20054
+ + '<div style="font-weight:500;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(f.title) + '</div>'
20055
+ + '<div style="font-size:11px;color:var(--text-muted);font-family:\\x27JetBrains Mono\\x27,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px">' + esc(f.relPath) + '</div>'
20056
+ + '</div>'
20057
+ + '<div style="display:flex;align-items:center;gap:8px;flex-shrink:0">'
20058
+ + typeBadge + agentBadge
20059
+ + '<span style="font-size:11px;color:var(--text-muted);min-width:60px;text-align:right">' + esc(timeAgo(f.mtime)) + '</span>'
20060
+ + '</div>'
20061
+ + '</div>';
20062
+ }
20063
+ html += '</div>';
20064
+ listEl.innerHTML = html;
20065
+ // Wire row clicks
20066
+ listEl.querySelectorAll('.vault-file-row').forEach(function(row) {
20067
+ row.onclick = function() { openVaultFile(row.getAttribute('data-path')); };
20068
+ });
20069
+ } catch (err) {
20070
+ listEl.innerHTML = '<div style="padding:24px;color:var(--red);font-size:13px">Failed to load: ' + esc(String(err)) + '</div>';
20071
+ }
20072
+ }
20073
+
20074
+ async function openVaultFile(relPath) {
20075
+ if (!relPath) return;
20076
+ // Build/reuse a slide-out drawer for content preview
20077
+ var drawer = document.getElementById('vault-file-drawer');
20078
+ if (!drawer) {
20079
+ drawer = document.createElement('div');
20080
+ drawer.id = 'vault-file-drawer';
20081
+ drawer.style.cssText = 'position:fixed;right:0;top:0;bottom:0;width:560px;max-width:92vw;background:var(--bg-secondary);border-left:1px solid var(--border);box-shadow:-8px 0 32px rgba(0,0,0,0.18);z-index:200;display:flex;flex-direction:column;transform:translateX(100%);transition:transform 200ms ease';
20082
+ drawer.innerHTML =
20083
+ '<div style="display:flex;align-items:center;gap:10px;padding:14px 18px;border-bottom:1px solid var(--border);flex-shrink:0">'
20084
+ + '<div style="flex:1;min-width:0">'
20085
+ + '<div id="vault-file-drawer-title" style="font-weight:600;font-size:15px;letter-spacing:-0.01em"></div>'
20086
+ + '<div id="vault-file-drawer-path" style="font-size:11px;color:var(--text-muted);font-family:\\x27JetBrains Mono\\x27,monospace;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></div>'
20087
+ + '</div>'
20088
+ + '<button class="btn-icon btn-sm" onclick="closeVaultFileDrawer()" title="Close">' + lucide('x', 'icn-sm') + '</button>'
20089
+ + '</div>'
20090
+ + '<div id="vault-file-drawer-body" style="flex:1;overflow-y:auto;padding:18px 22px;font-size:13px;line-height:1.55"></div>';
20091
+ document.body.appendChild(drawer);
20092
+ }
20093
+ var titleEl = document.getElementById('vault-file-drawer-title');
20094
+ var pathEl = document.getElementById('vault-file-drawer-path');
20095
+ var body = document.getElementById('vault-file-drawer-body');
20096
+ if (titleEl) titleEl.textContent = relPath.split('/').pop().replace(/\\.md$/, '');
20097
+ if (pathEl) pathEl.textContent = relPath;
20098
+ if (body) body.innerHTML = '<div class="skel-block"><div class="skel-row"></div><div class="skel-row med"></div><div class="skel-row short"></div></div>';
20099
+ drawer.style.transform = 'translateX(0)';
20100
+ try {
20101
+ var r = await apiFetch('/api/vault-file?path=' + encodeURIComponent(relPath));
20102
+ var d = await r.json();
20103
+ if (d.error) {
20104
+ body.innerHTML = '<div style="color:var(--red)">' + esc(d.error) + '</div>';
20105
+ return;
20106
+ }
20107
+ body.innerHTML = renderMd(d.content);
20108
+ } catch (err) {
20109
+ body.innerHTML = '<div style="color:var(--red)">Failed: ' + esc(String(err)) + '</div>';
20110
+ }
20111
+ }
20112
+
20113
+ function closeVaultFileDrawer() {
20114
+ var drawer = document.getElementById('vault-file-drawer');
20115
+ if (drawer) drawer.style.transform = 'translateX(100%)';
20116
+ }
20117
+
20118
+ function setVaultFolderFilter(folder) {
20119
+ _vaultFilesFolder = folder || '';
20120
+ refreshVaultFiles();
20121
+ }
20122
+
19819
20123
  // ── Goals: inline create form ────────────────────────────────────
19820
20124
  function openNewGoalForm() {
19821
20125
  var el = document.getElementById('new-goal-form');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",