clementine-agent 1.18.112 → 1.18.113

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.
@@ -2700,149 +2700,6 @@ export async function cmdDashboard(opts) {
2700
2700
  const needsRestart = currentHash !== buildHash;
2701
2701
  res.json({ hash: currentHash, started: buildHash, needsRestart });
2702
2702
  });
2703
- // ── Batch init — single request for all page-load data ───────────
2704
- // Eliminates 12+ concurrent requests that were saturating the event loop.
2705
- app.get('/api/init', async (_req, res) => {
2706
- try {
2707
- const result = {};
2708
- // Version
2709
- let currentHash = buildHash;
2710
- try {
2711
- const currentMtime = String(Math.floor(statSync(distDashboard).mtimeMs));
2712
- const gitHash = execSync('git rev-parse --short HEAD', { cwd: PACKAGE_ROOT, encoding: 'utf-8', timeout: 3000 }).trim();
2713
- currentHash = gitHash + '-' + currentMtime;
2714
- }
2715
- catch {
2716
- try {
2717
- currentHash = String(Math.floor(statSync(distDashboard).mtimeMs));
2718
- }
2719
- catch { /* use cached */ }
2720
- }
2721
- result.version = { hash: currentHash, started: buildHash, needsRestart: currentHash !== buildHash };
2722
- // Status
2723
- result.status = getStatus();
2724
- // Activity (default: no filters, limit 50)
2725
- try {
2726
- result.activity = cached('activity::::', 5_000, () => {
2727
- const events = [];
2728
- const runsDir = path.join(BASE_DIR, 'cron', 'runs');
2729
- if (existsSync(runsDir)) {
2730
- const files = readdirSync(runsDir).filter(f => f.endsWith('.jsonl'));
2731
- for (const file of files) {
2732
- const jobName = file.replace('.jsonl', '');
2733
- const colonIdx = jobName.indexOf(':');
2734
- const slug = colonIdx > 0 ? jobName.substring(0, colonIdx) : null;
2735
- const filePath = path.join(runsDir, file);
2736
- try {
2737
- const lines = readFileSync(filePath, 'utf-8').trim().split('\n').filter(Boolean);
2738
- for (const line of lines.slice(-10)) {
2739
- try {
2740
- const entry = JSON.parse(line);
2741
- events.push({
2742
- source: 'cron', eventType: 'cron_run', agentSlug: slug,
2743
- title: jobName, body: entry.summary ?? '', timestamp: entry.timestamp ?? '',
2744
- status: entry.success ? 'success' : 'error',
2745
- });
2746
- }
2747
- catch { /* skip */ }
2748
- }
2749
- }
2750
- catch { /* skip */ }
2751
- }
2752
- }
2753
- events.sort((a, b) => String(b.timestamp).localeCompare(String(a.timestamp)));
2754
- return { events: events.slice(0, 50) };
2755
- });
2756
- }
2757
- catch {
2758
- result.activity = { events: [] };
2759
- }
2760
- try {
2761
- result.metrics = computeMetrics();
2762
- }
2763
- catch {
2764
- result.metrics = {};
2765
- }
2766
- try {
2767
- const today = new Date();
2768
- const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
2769
- const planPath = path.join(PLANS_DIR, `${dateStr}.json`);
2770
- result.plan = existsSync(planPath) ? { ok: true, plan: JSON.parse(readFileSync(planPath, 'utf-8')) } : { ok: false, plan: null };
2771
- }
2772
- catch {
2773
- result.plan = { ok: false, plan: null };
2774
- }
2775
- try {
2776
- result.mcpServers = { servers: discoverMcpServers() };
2777
- }
2778
- catch {
2779
- result.mcpServers = { servers: [] };
2780
- }
2781
- try {
2782
- result.claudeIntegrations = { integrations: getClaudeIntegrations() };
2783
- }
2784
- catch {
2785
- result.claudeIntegrations = { integrations: [] };
2786
- }
2787
- result.projects = { projects: cachedProjects ?? [] };
2788
- //
2789
- try {
2790
- const agDir = AGENTS_DIR;
2791
- const mgr = new AgentManager(agDir);
2792
- const allAgents = mgr.listAll();
2793
- // Bot statuses from disk
2794
- let botStatuses = {};
2795
- try {
2796
- const p = path.join(BASE_DIR, '.bot-status.json');
2797
- if (existsSync(p))
2798
- botStatuses = JSON.parse(readFileSync(p, 'utf-8'));
2799
- }
2800
- catch { /* */ }
2801
- let slackStatuses = {};
2802
- try {
2803
- const p = path.join(BASE_DIR, '.slack-bot-status.json');
2804
- if (existsSync(p))
2805
- slackStatuses = JSON.parse(readFileSync(p, 'utf-8'));
2806
- }
2807
- catch { /* */ }
2808
- const statusData = getStatus();
2809
- result.office = {
2810
- clementine: {
2811
- name: statusData.name,
2812
- status: statusData.alive ? 'online' : 'offline',
2813
- uptime: statusData.uptime || '',
2814
- currentActivity: statusData.currentActivity || 'Idle',
2815
- channels: statusData.channels || [],
2816
- sessions: { active: 0, totalExchanges: 0 },
2817
- crons: { total: 0, runsToday: 0, successRate: 100, jobs: [] },
2818
- tokens: { input: 0, output: 0 },
2819
- },
2820
- agents: allAgents.map(a => ({
2821
- slug: a.slug,
2822
- name: a.name,
2823
- description: a.description,
2824
- status: a.status ?? 'active',
2825
- avatar: a.avatar ?? null,
2826
- model: a.model ?? null,
2827
- project: a.project ?? null,
2828
- agentDir: mgr.getAgentDir(a.slug),
2829
- botStatus: botStatuses[a.slug]?.status ?? null,
2830
- slackBotStatus: slackStatuses[a.slug]?.status ?? null,
2831
- sessions: { active: 0, totalExchanges: 0 },
2832
- crons: { total: 0, runsToday: 0, successRate: 100, jobs: [] },
2833
- tokens: { input: 0, output: 0 },
2834
- })),
2835
- };
2836
- }
2837
- catch {
2838
- result.office = { clementine: { name: 'Clementine', status: 'offline' }, agents: [] };
2839
- }
2840
- res.json(result);
2841
- }
2842
- catch (err) {
2843
- res.status(500).json({ error: String(err) });
2844
- }
2845
- });
2846
2703
  app.get('/api/status', (_req, res) => {
2847
2704
  res.json(getStatus());
2848
2705
  });
@@ -6846,17 +6703,9 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
6846
6703
  }
6847
6704
  });
6848
6705
  // ── CRON CRUD routes ──────────────────────────────────────────
6849
- app.get('/api/projects', (_req, res) => {
6850
- try {
6851
- // Use background-scanned projects — sync scanning blocks the event loop
6852
- const projects = cachedProjects ?? [];
6853
- const merged = projects;
6854
- res.json({ projects: merged });
6855
- }
6856
- catch (err) {
6857
- res.status(500).json({ error: String(err) });
6858
- }
6859
- });
6706
+ // (Dead duplicate /api/projects handler removed in 1.18.113 — first
6707
+ // registration at line 6183 is the live one; Express ignores later
6708
+ // same-method same-path registrations.)
6860
6709
  app.post('/api/projects/link', (req, res) => {
6861
6710
  try {
6862
6711
  const { path: projPath, description, keywords } = req.body;
@@ -10231,73 +10080,6 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
10231
10080
  res.status(500).json({ error: String(err) });
10232
10081
  }
10233
10082
  });
10234
- app.get('/api/skills', async (_req, res) => {
10235
- try {
10236
- const skillsDir = path.join(VAULT_DIR, '00-System', 'skills');
10237
- if (!existsSync(skillsDir)) {
10238
- res.json({ skills: [] });
10239
- return;
10240
- }
10241
- // Aggregate last-7-day retrieval stats from skill_usage table (best-effort).
10242
- const usageStats = new Map();
10243
- if (existsSync(MEMORY_DB_PATH)) {
10244
- try {
10245
- const Database = (await import('better-sqlite3')).default;
10246
- const db = new Database(MEMORY_DB_PATH, { readonly: true });
10247
- try {
10248
- const rows = db.prepare(`SELECT skill_name,
10249
- COUNT(*) AS retrievals,
10250
- MAX(retrieved_at) AS last_retrieved_at,
10251
- AVG(score) AS avg_score
10252
- FROM skill_usage
10253
- WHERE retrieved_at >= datetime('now', '-7 days')
10254
- GROUP BY skill_name`).all();
10255
- for (const r of rows) {
10256
- usageStats.set(r.skill_name, {
10257
- retrievals7d: r.retrievals,
10258
- lastRetrievedAt: r.last_retrieved_at,
10259
- avgScore: r.avg_score,
10260
- });
10261
- }
10262
- }
10263
- catch { /* skill_usage may not exist on older DBs */ }
10264
- db.close();
10265
- }
10266
- catch { /* non-fatal */ }
10267
- }
10268
- const files = readdirSync(skillsDir).filter(f => f.endsWith('.md'));
10269
- const skills = files.map(f => {
10270
- try {
10271
- const parsed = matter(readFileSync(path.join(skillsDir, f), 'utf-8'));
10272
- const name = f.replace('.md', '');
10273
- const stats = usageStats.get(name);
10274
- return {
10275
- name,
10276
- title: parsed.data.title ?? f,
10277
- description: parsed.data.description ?? '',
10278
- source: parsed.data.source ?? 'unknown',
10279
- sourceJob: parsed.data.sourceJob ?? null,
10280
- triggers: parsed.data.triggers ?? [],
10281
- toolsUsed: parsed.data.toolsUsed ?? [],
10282
- useCount: parsed.data.useCount ?? 0,
10283
- lastUsed: parsed.data.lastUsed ?? null,
10284
- createdAt: parsed.data.createdAt ?? '',
10285
- updatedAt: parsed.data.updatedAt ?? '',
10286
- retrievals7d: stats?.retrievals7d ?? 0,
10287
- lastRetrievedAt: stats?.lastRetrievedAt ?? null,
10288
- avgScore: stats?.avgScore ?? null,
10289
- };
10290
- }
10291
- catch {
10292
- return null;
10293
- }
10294
- }).filter(Boolean);
10295
- res.json({ skills });
10296
- }
10297
- catch (err) {
10298
- res.status(500).json({ error: String(err) });
10299
- }
10300
- });
10301
10083
  app.post('/api/skills', (req, res) => {
10302
10084
  try {
10303
10085
  const { title, description, triggers, steps } = req.body;
@@ -10342,57 +10124,6 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
10342
10124
  res.status(500).json({ error: String(err) });
10343
10125
  }
10344
10126
  });
10345
- app.get('/api/skills/:name', (req, res) => {
10346
- try {
10347
- // Check agent-scoped first (via query param), then global
10348
- const agentSlug = req.query.agent;
10349
- let filePath;
10350
- let skillDir;
10351
- if (agentSlug) {
10352
- const agentPath = path.join(VAULT_DIR, '00-System', 'agents', agentSlug, 'skills', `${req.params.name}.md`);
10353
- if (existsSync(agentPath)) {
10354
- filePath = agentPath;
10355
- skillDir = path.join(VAULT_DIR, '00-System', 'agents', agentSlug, 'skills');
10356
- }
10357
- else {
10358
- filePath = path.join(VAULT_DIR, '00-System', 'skills', `${req.params.name}.md`);
10359
- skillDir = path.join(VAULT_DIR, '00-System', 'skills');
10360
- }
10361
- }
10362
- else {
10363
- filePath = path.join(VAULT_DIR, '00-System', 'skills', `${req.params.name}.md`);
10364
- skillDir = path.join(VAULT_DIR, '00-System', 'skills');
10365
- }
10366
- if (!existsSync(filePath)) {
10367
- res.status(404).json({ error: 'Skill not found' });
10368
- return;
10369
- }
10370
- const matterMod = require('gray-matter');
10371
- const parsed = matterMod(readFileSync(filePath, 'utf-8'));
10372
- // Extract steps from content (after "## Procedure" heading)
10373
- const procMatch = parsed.content.match(/## Procedure\s*\n([\s\S]*)/);
10374
- const steps = procMatch ? procMatch[1].trim() : parsed.content.trim();
10375
- // Load attachment file list with base64 content for builder reload
10376
- const attachments = [];
10377
- const filesDir = path.join(skillDir, `${req.params.name}.files`);
10378
- if (existsSync(filesDir)) {
10379
- for (const f of readdirSync(filesDir)) {
10380
- try {
10381
- const fp = path.join(filesDir, f);
10382
- const stat = statSync(fp);
10383
- if (stat.isFile() && stat.size < 10 * 1024 * 1024) {
10384
- attachments.push({ filename: f, content: readFileSync(fp).toString('base64'), size: stat.size });
10385
- }
10386
- }
10387
- catch { /* skip */ }
10388
- }
10389
- }
10390
- res.json({ ...parsed.data, name: req.params.name, content: parsed.content, steps, attachmentFiles: attachments });
10391
- }
10392
- catch (err) {
10393
- res.status(500).json({ error: String(err) });
10394
- }
10395
- });
10396
10127
  // ── Agent-scoped Skills ──
10397
10128
  app.get('/api/agents/:slug/skills', (req, res) => {
10398
10129
  try {
@@ -13602,123 +13333,6 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
13602
13333
  }
13603
13334
 
13604
13335
  /* Right rail */
13605
- .home-rail {
13606
- display: flex;
13607
- flex-direction: column;
13608
- gap: 8px;
13609
- overflow-y: auto;
13610
- position: relative;
13611
- }
13612
- .home-rail.collapsed {
13613
- display: none;
13614
- }
13615
- /* Auto-hide cards that have no actionable content (set via JS toggling .rail-card.empty) */
13616
- .rail-card.empty { display: none; }
13617
- .rail-collapse-btn {
13618
- position: absolute;
13619
- top: -4px;
13620
- right: -4px;
13621
- background: none;
13622
- border: 1px solid var(--border);
13623
- color: var(--text-muted);
13624
- width: 22px;
13625
- height: 22px;
13626
- border-radius: 50%;
13627
- cursor: pointer;
13628
- font-size: 14px;
13629
- display: none;
13630
- align-items: center;
13631
- justify-content: center;
13632
- z-index: 5;
13633
- }
13634
- .rail-card {
13635
- background: var(--bg-card);
13636
- border: 1px solid var(--border);
13637
- border-radius: var(--radius-md);
13638
- overflow: hidden;
13639
- box-shadow: var(--shadow-xs);
13640
- }
13641
- .rail-header {
13642
- display: flex;
13643
- align-items: center;
13644
- justify-content: space-between;
13645
- padding: 8px 12px;
13646
- font-size: var(--text-xs);
13647
- font-weight: 600;
13648
- text-transform: uppercase;
13649
- letter-spacing: 0.04em;
13650
- color: var(--text-muted);
13651
- background: transparent;
13652
- }
13653
- .rail-body { padding: 8px 12px 10px; font-size: var(--text-sm); line-height: 1.45; }
13654
- .rail-body .empty-state, .rail-body .skel-row { font-size: var(--text-xs); }
13655
- .rail-badge {
13656
- display: inline-flex;
13657
- align-items: center;
13658
- justify-content: center;
13659
- min-width: 18px;
13660
- height: 18px;
13661
- padding: 0 6px;
13662
- border-radius: 9px;
13663
- background: var(--clementine);
13664
- color: #fff;
13665
- font-size: 10px;
13666
- font-weight: 600;
13667
- }
13668
- .rail-row {
13669
- display: flex;
13670
- align-items: center;
13671
- gap: 8px;
13672
- padding: 6px 0;
13673
- font-size: 12px;
13674
- border-bottom: 1px dashed var(--border);
13675
- }
13676
- .rail-row:last-child { border-bottom: none; }
13677
- .rail-row .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
13678
- .rail-row .meta { font-size: 10px; color: var(--text-muted); flex-shrink: 0; }
13679
-
13680
- /* Floating "open rail" button when collapsed */
13681
- .home-rail-toggle {
13682
- position: fixed;
13683
- top: 80px;
13684
- right: 18px;
13685
- z-index: 100;
13686
- width: 36px;
13687
- height: 36px;
13688
- border-radius: 50%;
13689
- background: var(--clementine);
13690
- color: #fff;
13691
- border: none;
13692
- box-shadow: 0 2px 8px rgba(0,0,0,0.2);
13693
- cursor: pointer;
13694
- font-size: 14px;
13695
- display: none;
13696
- }
13697
- .home-rail.collapsed ~ .home-rail-toggle,
13698
- .home-rail.collapsed + .home-rail-toggle { display: block; }
13699
-
13700
- /* Narrow screens: rail becomes a slide-out drawer */
13701
- @media (max-width: 1024px) {
13702
- .home-layout { grid-template-columns: 1fr; }
13703
- .home-rail {
13704
- position: fixed;
13705
- right: 0;
13706
- top: var(--header-h);
13707
- bottom: 0;
13708
- width: 320px;
13709
- max-width: 90vw;
13710
- transform: translateX(100%);
13711
- transition: transform 0.2s ease;
13712
- background: var(--bg);
13713
- border-left: 1px solid var(--border);
13714
- box-shadow: -4px 0 20px rgba(0,0,0,0.15);
13715
- padding: 14px;
13716
- z-index: 50;
13717
- }
13718
- .home-rail.open { transform: translateX(0); }
13719
- .rail-collapse-btn { display: flex; }
13720
- .home-rail-toggle { display: block; }
13721
- .home-rail.open ~ .home-rail-toggle { display: none; }
13722
13336
  }
13723
13337
 
13724
13338
  /* ── Cards ──────────────────────────────── */
@@ -22283,11 +21897,6 @@ function navigateTo(page, opts) {
22283
21897
  if (t === 'chat') {
22284
21898
  var ci = document.getElementById('chat-input');
22285
21899
  if (ci) ci.focus();
22286
- } else if (t === 'today') {
22287
- var rail = document.getElementById('home-rail');
22288
- if (rail && window.matchMedia('(max-width: 1024px)').matches) rail.classList.add('open');
22289
- var p = document.getElementById('home-plan-content');
22290
- if (p) p.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
22291
21900
  } else if (t === 'activity') {
22292
21901
  var act = document.getElementById('panel-activity');
22293
21902
  if (act) act.scrollIntoView({ behavior: 'smooth', block: 'start' });
@@ -24480,16 +24089,6 @@ function operationSectionHeader(title, subtitle, badgeClass, badgeText, marginTo
24480
24089
  + '</div>';
24481
24090
  }
24482
24091
 
24483
- function renderOperationsSummary(ops) {
24484
- var s = ops.summary || {};
24485
- return '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-bottom:16px">'
24486
- + '<div style="border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);padding:10px 12px"><div style="font-size:11px;color:var(--text-muted)">Needs Attention</div><div style="font-size:20px;font-weight:700;color:' + ((s.needsAttention || 0) > 0 ? 'var(--red)' : 'var(--green)') + '">' + esc(s.needsAttention || 0) + '</div></div>'
24487
- + '<div style="border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);padding:10px 12px"><div style="font-size:11px;color:var(--text-muted)">Scheduled Tasks</div><div style="font-size:20px;font-weight:700">' + esc(s.enabledScheduledTasks || 0) + '/' + esc(s.scheduledTasks || 0) + '</div></div>'
24488
- + '<div style="border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);padding:10px 12px"><div style="font-size:11px;color:var(--text-muted)">Scheduled Workflows</div><div style="font-size:20px;font-weight:700">' + esc(s.enabledScheduledWorkflows || 0) + '/' + esc(s.scheduledWorkflows || 0) + '</div></div>'
24489
- + '<div style="border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);padding:10px 12px"><div style="font-size:11px;color:var(--text-muted)">Running Now</div><div style="font-size:20px;font-weight:700;color:' + ((s.runningNow || 0) > 0 ? 'var(--blue)' : 'var(--text-primary)') + '">' + esc(s.runningNow || 0) + '</div></div>'
24490
- + '<div style="border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);padding:10px 12px"><div style="font-size:11px;color:var(--text-muted)">Scheduled Tokens</div><div style="font-size:20px;font-weight:700">' + esc(formatTokens(s.automationTokens || 0)) + '</div></div>'
24491
- + '</div>';
24492
- }
24493
24092
 
24494
24093
  function renderAttentionCard(item) {
24495
24094
  var broken = item.brokenJob || null;
@@ -26035,7 +25634,6 @@ async function refreshCron() {
26035
25634
  // Reliability (failures stacked by category). Filled in by
26036
25635
  // refreshMiniDashboards from the same /api/cron/runs payload.
26037
25636
  html += '<div id="mini-dashboards" class="mini-dashboards"></div>';
26038
- html += renderOperationsSummary(ops);
26039
25637
 
26040
25638
  // ── Zone 1 — Running now (promoted to top, primary "what's live" view) ──
26041
25639
  if (visibleRunning.length > 0) {
@@ -29771,28 +29369,6 @@ function setScheduleFromCron(expr) {
29771
29369
  updateScheduleFromBuilder();
29772
29370
 
29773
29371
  // ── Timers ────────────────────────────────
29774
- async function refreshTimers() {
29775
- try {
29776
- const r = await apiFetch('/api/timers');
29777
- const d = await r.json();
29778
- const count = Array.isArray(d) ? d.length : 0;
29779
- var _tc = document.getElementById('nav-timer-count'); if (_tc) _tc.textContent = count;
29780
- var _ttc = document.getElementById('tab-timer-count'); if (_ttc) { _ttc.textContent = count; _ttc.style.display = count > 0 ? '' : 'none'; }
29781
- if (!Array.isArray(d) || d.length === 0) {
29782
- document.getElementById('panel-timers').innerHTML = '<div class="empty-state">No pending timers</div>';
29783
- return;
29784
- }
29785
- let html = '<table><tr><th>ID</th><th>Fires At</th><th>Message</th><th style="width:80px"></th></tr>';
29786
- for (const t of d) {
29787
- html += '<tr><td><code>' + esc(t.id || '?') + '</code></td>'
29788
- + '<td>' + esc(t.fireAt || t.fire_at || t.time || '') + '</td>'
29789
- + '<td>' + esc((t.message || t.prompt || '').slice(0, 100)) + '</td>'
29790
- + '<td><button class="btn-danger btn-sm" onclick="apiPost(\\x27/api/timers/' + encodeURIComponent(t.id) + '/cancel\\x27)">Cancel</button></td></tr>';
29791
- }
29792
- html += '</table>';
29793
- document.getElementById('panel-timers').innerHTML = html;
29794
- } catch(e) { }
29795
- }
29796
29372
 
29797
29373
  // ── Activity Feed ─────────────────────────
29798
29374
  var activityLastTimestamp = '';
@@ -34987,132 +34563,6 @@ function briefingNeedsReviewClick(href) {
34987
34563
  }
34988
34564
  }
34989
34565
 
34990
- function toggleHomeRail() {
34991
- var rail = document.getElementById('home-rail');
34992
- if (!rail) return;
34993
- // Mobile: open/close. Desktop: collapse/show.
34994
- if (window.matchMedia('(max-width: 1024px)').matches) {
34995
- rail.classList.toggle('open');
34996
- } else {
34997
- rail.classList.toggle('collapsed');
34998
- }
34999
- }
35000
-
35001
- function _railCard(bodyId) {
35002
- var body = document.getElementById(bodyId);
35003
- return body ? body.closest('.rail-card') : null;
35004
- }
35005
- function _setRailEmpty(bodyId, isEmpty) {
35006
- var card = _railCard(bodyId);
35007
- if (card) card.classList.toggle('empty', !!isEmpty);
35008
- }
35009
-
35010
- async function refreshHomeRail() {
35011
- // Daemon status — only surface when explicitly stopped. Treat null/undefined
35012
- // (running-state unknown) as "fine, hide" since the dashboard wouldn't be
35013
- // serving requests if the daemon were truly down.
35014
- try {
35015
- var rs = await apiFetch('/api/status');
35016
- var ds = await rs.json();
35017
- var stopped = ds.running === false;
35018
- var pip = document.querySelector('#rail-daemon-body .agent-activity-dot');
35019
- var label = document.querySelector('#rail-daemon-body .agent-activity span:last-child');
35020
- if (label) label.textContent = stopped ? 'Daemon stopped' : 'Running';
35021
- if (pip) pip.style.background = stopped ? '#ef4444' : '#22c55e';
35022
- var up = document.getElementById('rail-daemon-uptime');
35023
- if (up && ds.uptimeMs) up.textContent = Math.round(ds.uptimeMs / 60000) + 'm';
35024
- _setRailEmpty('rail-daemon-body', !stopped);
35025
- } catch { _setRailEmpty('rail-daemon-body', true); }
35026
-
35027
- // Today's plan (compact). Hide card if no plan or zero items.
35028
- try {
35029
- var rp = await apiFetch('/api/daily-plan');
35030
- var dp = await rp.json();
35031
- var planEl = document.getElementById('home-plan-content');
35032
- var items = dp && dp.plan ? (dp.plan.items || []) : [];
35033
- if (planEl) {
35034
- if (items.length === 0) {
35035
- planEl.innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No plan yet today.</div>';
35036
- } else {
35037
- planEl.innerHTML = items.slice(0, 4).map(function(it) {
35038
- return '<div class="rail-row"><span class="label">' + esc(it.title || it.text || '') + '</span><span class="meta">' + esc(it.time || '') + '</span></div>';
35039
- }).join('');
35040
- }
35041
- }
35042
- _setRailEmpty('home-plan-content', items.length === 0);
35043
- } catch {
35044
- _setRailEmpty('home-plan-content', true);
35045
- }
35046
-
35047
- // Upcoming cron fires (next 3) — hide card if nothing scheduled
35048
- try {
35049
- var rc = await apiFetch('/api/cron');
35050
- var dc = await rc.json();
35051
- var jobs = (dc.jobs || []).filter(function(j) { return j.enabled && j.nextRun; });
35052
- jobs.sort(function(a, b) { return new Date(a.nextRun).getTime() - new Date(b.nextRun).getTime(); });
35053
- var top = jobs.slice(0, 3);
35054
- var ue = document.getElementById('rail-upcoming');
35055
- var uc = document.getElementById('rail-upcoming-count');
35056
- if (uc) uc.textContent = String(jobs.length);
35057
- if (ue) {
35058
- ue.innerHTML = top.map(function(j) {
35059
- return '<div class="rail-row clickable-row" onclick="navigateTo(\\x27build\\x27,{tab:\\x27crons\\x27})"><span class="label">' + esc(j.name) + '</span><span class="meta">' + esc(timeUntil(j.nextRun)) + '</span></div>';
35060
- }).join('');
35061
- }
35062
- _setRailEmpty('rail-upcoming', top.length === 0);
35063
- } catch { _setRailEmpty('rail-upcoming', true); }
35064
-
35065
- // Active unleashed runs — hide card unless something running
35066
- try {
35067
- var ru = await apiFetch('/api/unleashed');
35068
- var du = await ru.json();
35069
- var active = (du.tasks || []).filter(function(t) { return t.live === true || t.runtimeState === 'active'; });
35070
- var ae = document.getElementById('rail-active');
35071
- var ac = document.getElementById('rail-active-count');
35072
- if (ac) {
35073
- if (active.length > 0) { ac.style.display = ''; ac.textContent = String(active.length); }
35074
- else ac.style.display = 'none';
35075
- }
35076
- if (ae) {
35077
- ae.innerHTML = active.map(function(t) {
35078
- return '<div class="rail-row clickable-row" onclick="navigateTo(\\x27build\\x27,{tab:\\x27crons\\x27})"><span class="label">' + esc(t.name) + '</span><span class="meta">' + esc(t.phase || '') + '</span></div>';
35079
- }).join('');
35080
- }
35081
- _setRailEmpty('rail-active', active.length === 0);
35082
- } catch { _setRailEmpty('rail-active', true); }
35083
-
35084
- // Time saved (compact). Hide if zero.
35085
- try {
35086
- var rm = await apiFetch('/api/metrics?period=week');
35087
- var dm = await rm.json();
35088
- var minutes = ((dm.cronRuns || 0) * 5) + ((dm.exchanges || 0) * 2);
35089
- var ts = document.getElementById('rail-time-saved');
35090
- if (ts) {
35091
- if (minutes >= 60) ts.innerHTML = '<div style="font-size:var(--text-md);font-weight:600">' + (minutes / 60).toFixed(1) + 'h</div><div style="font-size:11px;color:var(--text-muted)">' + (dm.cronRuns || 0) + ' runs · ' + (dm.exchanges || 0) + ' chats</div>';
35092
- else ts.innerHTML = '<div style="font-size:var(--text-md);font-weight:600">' + minutes + 'm</div><div style="font-size:11px;color:var(--text-muted)">' + (dm.cronRuns || 0) + ' runs</div>';
35093
- }
35094
- _setRailEmpty('rail-time-saved', minutes === 0);
35095
- } catch { _setRailEmpty('rail-time-saved', true); }
35096
-
35097
- // Approvals — hide card unless something pending
35098
- try {
35099
- var rsi = await apiFetch('/api/self-improve');
35100
- var dsi = await rsi.json();
35101
- var pending = (dsi.proposals || []).filter(function(p) { return p.status === 'pending'; });
35102
- var ae2 = document.getElementById('rail-approvals');
35103
- var ac2 = document.getElementById('rail-approvals-count');
35104
- if (ac2) {
35105
- if (pending.length > 0) { ac2.style.display = ''; ac2.textContent = String(pending.length); }
35106
- else ac2.style.display = 'none';
35107
- }
35108
- if (ae2) {
35109
- ae2.innerHTML = pending.slice(0, 3).map(function(p) {
35110
- return '<div class="rail-row clickable-row" onclick="navigateTo(\\x27brain\\x27,{tab:\\x27learning\\x27})"><span class="label">' + esc(p.area || 'proposal') + ': ' + esc((p.target || '').slice(0, 40)) + '</span><span class="meta">' + esc(((p.score || 0) * 100).toFixed(0)) + '%</span></div>';
35111
- }).join('');
35112
- }
35113
- _setRailEmpty('rail-approvals', pending.length === 0);
35114
- } catch { _setRailEmpty('rail-approvals', true); }
35115
- }
35116
34566
 
35117
34567
  function timeUntil(iso) {
35118
34568
  if (!iso) return '';
@@ -35134,7 +34584,6 @@ async function refreshAll() {
35134
34584
  else refreshActivity(); // Fall back to direct /api/activity fetch when init didn't include it
35135
34585
  if (d.office) refreshTeamNav(d.office);
35136
34586
  // Home rail data — fire and forget, doesn't block init render.
35137
- if (currentPage === 'home') refreshHomeRail();
35138
34587
  if (d.version) {
35139
34588
  if (d.version.needsRestart && !_restartBannerShown) {
35140
34589
  _restartBannerShown = true;
@@ -38297,34 +37746,6 @@ async function refreshHomeMetrics() {
38297
37746
  }
38298
37747
 
38299
37748
  // ── Home Page: Sessions Tab ──────────────
38300
- async function refreshHomeSessions() {
38301
- var container = document.getElementById('panel-sessions-home');
38302
- if (!container) return;
38303
- try {
38304
- var r = await apiFetch('/api/sessions');
38305
- var d = await r.json();
38306
- var keys = Object.keys(d);
38307
- if (keys.length === 0) { container.innerHTML = '<div class="empty-state">No active sessions</div>'; return; }
38308
- var html = '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px">';
38309
- keys.forEach(function(key) {
38310
- var s = d[key];
38311
- var icon = key.indexOf('discord') >= 0 ? '&#128172;' : key.indexOf('slack') >= 0 ? '&#128488;' : key.indexOf('telegram') >= 0 ? '&#9992;' : key.indexOf('dashboard') >= 0 ? '&#127760;' : '&#128172;';
38312
- var exchanges = s.exchanges || 0;
38313
- var lastActive = s.lastActive ? fmtTimeAgo(s.lastActive) : 'unknown';
38314
- html += '<div class="card" style="padding:12px;cursor:pointer" onclick="viewSessionModal(\\x27' + encodeURIComponent(key) + '\\x27)">';
38315
- html += '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">';
38316
- html += '<span style="font-size:16px">' + icon + '</span>';
38317
- html += '<span style="font-weight:500;font-size:13px">' + esc(key.split(':').pop() || key) + '</span>';
38318
- html += '</div>';
38319
- html += '<div style="font-size:12px;color:var(--text-muted)">' + exchanges + ' exchanges · ' + lastActive + '</div>';
38320
- html += '</div>';
38321
- });
38322
- html += '</div>';
38323
- container.innerHTML = html;
38324
- } catch(e) {
38325
- container.innerHTML = '<div class="empty-state">Failed to load sessions</div>';
38326
- }
38327
- }
38328
37749
 
38329
37750
  // ── Execution Analytics ───────────────────
38330
37751
  async function refreshAdvisorAnalytics() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.112",
3
+ "version": "1.18.113",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",