clementine-agent 1.18.112 → 1.18.114

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.
Files changed (2) hide show
  1. package/dist/cli/dashboard.js +242 -637
  2. package/package.json +1 -1
@@ -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
  });
@@ -6601,6 +6458,44 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
6601
6458
  res.status(500).json({ error: String(err) });
6602
6459
  }
6603
6460
  });
6461
+ // POST /api/tools/probe — force-refresh the SDK tool inventory cache
6462
+ // (`~/.clementine/.tool-inventory.json`). Without this the catalog page's
6463
+ // Claude Desktop connectors (claude_ai_*) and per-server tool counts stay
6464
+ // empty until the daemon happens to fire its first agent run. The probe
6465
+ // takes ~3-8s because it boots a real SDK query against haiku to capture
6466
+ // `system/init.tools`. Cached results are returned without re-probing
6467
+ // unless `force=true` is passed.
6468
+ app.post('/api/tools/probe', async (req, res) => {
6469
+ try {
6470
+ const { probeAvailableTools } = await import('../agent/mcp-bridge.js');
6471
+ const force = req.body?.force === true;
6472
+ const inv = await probeAvailableTools(force);
6473
+ res.json({ ok: true, probedAt: inv.probedAt, toolCount: inv.tools.length, tools: inv.tools });
6474
+ }
6475
+ catch (err) {
6476
+ res.status(500).json({ ok: false, error: String(err?.message ?? err) });
6477
+ }
6478
+ });
6479
+ // GET /api/tools/inventory — cached SDK probe result. Returns the raw
6480
+ // tool list the SDK currently advertises (built-ins + every MCP server's
6481
+ // tools + claude_ai_* connectors + Composio toolkits). `null` when the
6482
+ // probe hasn't run yet. Reads the on-disk cache; never blocks on probing.
6483
+ app.get('/api/tools/inventory', (_req, res) => {
6484
+ try {
6485
+ // Inline the cache read to avoid module imports in the hot path.
6486
+ const TOOL_INVENTORY_FILE = path.join(BASE_DIR, '.tool-inventory.json');
6487
+ if (!existsSync(TOOL_INVENTORY_FILE)) {
6488
+ res.json({ probed: false, probedAt: null, toolCount: 0, tools: [] });
6489
+ return;
6490
+ }
6491
+ const data = JSON.parse(readFileSync(TOOL_INVENTORY_FILE, 'utf-8'));
6492
+ const tools = Array.isArray(data.tools) ? data.tools : [];
6493
+ res.json({ probed: true, probedAt: data.probedAt, toolCount: tools.length, tools });
6494
+ }
6495
+ catch (err) {
6496
+ res.status(500).json({ ok: false, error: String(err?.message ?? err) });
6497
+ }
6498
+ });
6604
6499
  // ── Composio (1000+ third-party services via OAuth broker) ────
6605
6500
  app.get('/api/composio/status', async (_req, res) => {
6606
6501
  // Use isComposioEnabled — checks both process.env (dashboard hot-reload)
@@ -6846,17 +6741,9 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
6846
6741
  }
6847
6742
  });
6848
6743
  // ── 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
- });
6744
+ // (Dead duplicate /api/projects handler removed in 1.18.113 — first
6745
+ // registration at line 6183 is the live one; Express ignores later
6746
+ // same-method same-path registrations.)
6860
6747
  app.post('/api/projects/link', (req, res) => {
6861
6748
  try {
6862
6749
  const { path: projPath, description, keywords } = req.body;
@@ -10231,73 +10118,6 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
10231
10118
  res.status(500).json({ error: String(err) });
10232
10119
  }
10233
10120
  });
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
10121
  app.post('/api/skills', (req, res) => {
10302
10122
  try {
10303
10123
  const { title, description, triggers, steps } = req.body;
@@ -10342,57 +10162,6 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
10342
10162
  res.status(500).json({ error: String(err) });
10343
10163
  }
10344
10164
  });
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
10165
  // ── Agent-scoped Skills ──
10397
10166
  app.get('/api/agents/:slug/skills', (req, res) => {
10398
10167
  try {
@@ -13602,123 +13371,6 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
13602
13371
  }
13603
13372
 
13604
13373
  /* 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
13374
  }
13723
13375
 
13724
13376
  /* ── Cards ──────────────────────────────── */
@@ -22283,11 +21935,6 @@ function navigateTo(page, opts) {
22283
21935
  if (t === 'chat') {
22284
21936
  var ci = document.getElementById('chat-input');
22285
21937
  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
21938
  } else if (t === 'activity') {
22292
21939
  var act = document.getElementById('panel-activity');
22293
21940
  if (act) act.scrollIntoView({ behavior: 'smooth', block: 'start' });
@@ -24480,16 +24127,6 @@ function operationSectionHeader(title, subtitle, badgeClass, badgeText, marginTo
24480
24127
  + '</div>';
24481
24128
  }
24482
24129
 
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
24130
 
24494
24131
  function renderAttentionCard(item) {
24495
24132
  var broken = item.brokenJob || null;
@@ -25700,85 +25337,231 @@ async function refreshToolsMcpCatalog() {
25700
25337
  var panel = document.getElementById('panel-toolsmcp');
25701
25338
  if (!panel) return;
25702
25339
  panel.innerHTML = '<div class="empty-state" style="padding:24px;color:var(--text-muted)">Loading Tools &amp; MCP catalog…</div>';
25703
- var statusMap = {};
25704
- var servers = [];
25340
+
25341
+ // Fetch every input the catalog needs in parallel:
25342
+ // - /api/available-tools — 12 categories of curated tool entries (Core SDK, CLI, Memory&Vault, Composio, etc.)
25343
+ // - /api/tools/inventory — the SDK probe cache (every tool the daemon's Claude Agent SDK can actually see, including claude_ai_* connectors)
25344
+ // - /api/mcp-servers — configured MCP servers (transport, command, env)
25345
+ // - /api/mcp-status — live connection status keyed by server name
25346
+ var avail = null, inventory = null, statusMap = {}, servers = [];
25705
25347
  try {
25706
- var sR = await apiFetch('/api/mcp-status');
25707
- var statusJson = await sR.json();
25708
- // /api/mcp-status returns { servers: [{name, status}], updatedAt }.
25709
- // Build a name entry lookup so renderMcpCatalogCard can probe by name.
25348
+ var results = await Promise.all([
25349
+ apiFetch('/api/available-tools').then(function(r){ return r.json(); }).catch(function(){ return null; }),
25350
+ apiFetch('/api/tools/inventory').then(function(r){ return r.json(); }).catch(function(){ return null; }),
25351
+ apiFetch('/api/mcp-status').then(function(r){ return r.json(); }).catch(function(){ return null; }),
25352
+ apiFetch('/api/mcp-servers').then(function(r){ return r.json(); }).catch(function(){ return null; }),
25353
+ ]);
25354
+ avail = results[0];
25355
+ inventory = results[1];
25356
+ var statusJson = results[2];
25710
25357
  if (statusJson && Array.isArray(statusJson.servers)) {
25711
25358
  for (var si = 0; si < statusJson.servers.length; si++) {
25712
25359
  var entry = statusJson.servers[si];
25713
25360
  if (entry && entry.name) statusMap[entry.name] = entry;
25714
25361
  }
25715
25362
  }
25716
- } catch (e) { /* status is optional — servers still render without it */ }
25717
- try {
25718
- var lR = await apiFetch('/api/mcp-servers');
25719
- var lJson = await lR.json();
25363
+ var lJson = results[3];
25720
25364
  servers = (lJson && lJson.servers) || [];
25721
25365
  } catch (e) {
25722
- panel.innerHTML = '<div class="empty-state" style="padding:24px;color:var(--red)">Failed to load MCP servers: ' + esc(String(e)) + '</div>';
25366
+ panel.innerHTML = '<div class="empty-state" style="padding:24px;color:var(--red)">Failed to load tool catalog: ' + esc(String(e)) + '</div>';
25723
25367
  return;
25724
25368
  }
25369
+
25370
+ // Total tools across every curated category. Composio toolkits are 1k+
25371
+ // in user installs — so the count includes them but the rendered list
25372
+ // collapses by default (see below).
25373
+ var totalTools = 0;
25374
+ var categoryEntries = [];
25375
+ if (avail && avail.categories && typeof avail.categories === 'object') {
25376
+ var keys = Object.keys(avail.categories);
25377
+ for (var ki = 0; ki < keys.length; ki++) {
25378
+ var arr = avail.categories[keys[ki]] || [];
25379
+ categoryEntries.push({ name: keys[ki], items: Array.isArray(arr) ? arr : [] });
25380
+ totalTools += Array.isArray(arr) ? arr.length : 0;
25381
+ }
25382
+ }
25383
+
25384
+ // SDK probe — when populated, gives us the live system/init.tools list
25385
+ // so each card can show its real tool count and the "Live" badge can flip
25386
+ // on for tools that are actually registered with the SDK right now.
25387
+ var liveTools = inventory && Array.isArray(inventory.tools) ? inventory.tools : [];
25388
+ var liveSet = {};
25389
+ for (var lt = 0; lt < liveTools.length; lt++) liveSet[liveTools[lt]] = true;
25390
+ var probedAt = inventory && inventory.probedAt;
25391
+
25725
25392
  var tabCount = document.getElementById('build-tab-toolsmcp-count');
25726
25393
  if (tabCount) {
25727
- tabCount.textContent = servers.length;
25728
- tabCount.style.display = servers.length > 0 ? '' : 'none';
25729
- }
25730
- // Bucket servers into the four PRD categories. The existing
25731
- // ManagedMcpServer type doesn't have an explicit "kind" field, so we
25732
- // infer: stdio with a known shell binary → 'shell', stdio bundled with
25733
- // clementine → 'builtin', stdio external command → 'external_stdio',
25734
- // http/sse → 'external_remote'. The bucket keys map to the PRD's four
25735
- // taxonomy cards.
25736
- var buckets = { builtin: [], custom: [], shell: [], external: [] };
25737
- for (var i = 0; i < servers.length; i++) {
25738
- var s = servers[i];
25739
- var name = s.name || '';
25740
- var type = s.type || 'stdio';
25741
- var cmd = s.command || '';
25742
- var kind;
25743
- // The clementine-tools server is an in-process bundle
25744
- if (name === 'clementine-tools' || name === 'kernel') kind = 'builtin';
25745
- else if (type === 'http' || type === 'sse') kind = 'external';
25746
- else if (/^(sf|gh|gcloud|kubectl|docker|aws|az|terraform)$/.test(cmd) || /\\b(sf|gh|gcloud|kubectl)$/.test(cmd)) kind = 'shell';
25747
- else kind = 'external'; // default for stdio external MCP
25748
- buckets[kind].push(s);
25394
+ tabCount.textContent = totalTools;
25395
+ tabCount.style.display = totalTools > 0 ? '' : 'none';
25749
25396
  }
25397
+
25750
25398
  var html = '';
25751
- // Header strip
25752
- html += '<div style="margin-bottom:18px"><h2 style="margin:0 0 4px;font-size:18px;font-weight:600;color:var(--text-primary)">Tools &amp; MCP catalog</h2>'
25753
- + '<div style="font-size:12px;color:var(--text-muted)">'+ esc(servers.length) +' MCP server' + (servers.length === 1 ? '' : 's') + ' configured. Click any task in the Tasks tab to bind specific tools to that task.</div></div>';
25754
- // Four-card taxonomy. Each section is a labeled bucket of cards.
25755
- var sections = [
25756
- { key: 'builtin', label: 'Built-in', desc: 'Claude SDK native tools — always available to every task at the agent profile\\x27s permission tier.' },
25757
- { key: 'custom', label: 'Custom in-process MCP', desc: 'MCP servers defined in clementine\\x27s code, loaded inside the daemon process.' },
25758
- { key: 'shell', label: 'Shell commands', desc: 'Local CLI binaries (sf, gh, gcloud…) wrapped as MCP servers.' },
25759
- { key: 'external', label: 'External MCP servers', desc: 'Third-party MCP servers reached over stdio, SSE, or HTTP.' },
25760
- ];
25761
- for (var k = 0; k < sections.length; k++) {
25762
- var sec = sections[k];
25763
- var bucket = buckets[sec.key] || [];
25764
- html += '<div style="margin-bottom:24px">';
25399
+
25400
+ // ── Header strip: total + Refresh button + last-probed timestamp ─────
25401
+ html += '<div style="display:flex;align-items:flex-end;gap:14px;margin-bottom:18px;flex-wrap:wrap">';
25402
+ html += '<div style="flex:1;min-width:240px">'
25403
+ + '<h2 style="margin:0 0 4px;font-size:18px;font-weight:600;color:var(--text-primary)">Tools &amp; MCP catalog</h2>'
25404
+ + '<div style="font-size:12px;color:var(--text-muted)">'
25405
+ + esc(totalTools) + ' tool' + (totalTools === 1 ? '' : 's') + ' across ' + esc(categoryEntries.length) + ' categories'
25406
+ + ' · ' + esc(servers.length) + ' MCP server' + (servers.length === 1 ? '' : 's')
25407
+ + ' · ' + esc(liveTools.length) + ' live in SDK'
25408
+ + (probedAt ? ' (probed ' + esc(timeAgo(probedAt)) + ')' : ' (not probed yet — click Refresh)')
25409
+ + '</div>'
25410
+ + '</div>';
25411
+ html += '<div style="display:flex;gap:8px;align-items:center">'
25412
+ + '<input id="toolsmcp-search" type="text" placeholder="Search tools…" oninput="filterToolsCatalog()" style="padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:12px;width:200px"/>'
25413
+ + '<button class="btn-sm" id="toolsmcp-probe-btn" onclick="probeToolsMcpInventory()" title="Run a one-shot SDK query to refresh the tool inventory (~5s). Picks up new connectors and any tool the SDK is currently surfacing.">Refresh inventory</button>'
25414
+ + '</div>';
25415
+ html += '</div>';
25416
+
25417
+ // ── Tool categories from /api/available-tools ─────────────────────────
25418
+ // Composio (1k+ entries) collapses by default; everything else opens.
25419
+ var compositeRender = function(entry) {
25420
+ var t = entry || {};
25421
+ var name = t.name || '(unnamed)';
25422
+ var desc = t.description || '';
25423
+ var typeLabel = t.type || '';
25424
+ var connected = t.connected;
25425
+ var apiName = t.api;
25426
+ var liveBadge = liveSet[name] ? '<span style="font-size:10px;color:var(--green);background:rgba(34,197,94,0.12);padding:1px 6px;border-radius:999px;margin-left:6px">LIVE</span>' : '';
25427
+ var connBadge = '';
25428
+ if (connected === true) connBadge = '<span style="font-size:10px;color:var(--green);background:rgba(34,197,94,0.12);padding:1px 6px;border-radius:999px;margin-left:6px">connected</span>';
25429
+ else if (connected === false) connBadge = '<span style="font-size:10px;color:var(--text-muted);background:rgba(148,163,184,0.12);padding:1px 6px;border-radius:999px;margin-left:6px">offline</span>';
25430
+ var typeBadge = typeLabel ? '<span style="font-size:10px;color:var(--text-muted);background:var(--bg-tertiary);padding:1px 6px;border-radius:3px;margin-left:6px;text-transform:uppercase;letter-spacing:0.04em">' + esc(typeLabel) + '</span>' : '';
25431
+ var apiBadge = apiName ? '<span style="font-size:10px;color:var(--text-muted);margin-left:6px">via ' + esc(apiName) + '</span>' : '';
25432
+ return '<div class="toolsmcp-tool" data-tool-name="' + esc(name.toLowerCase()) + '" data-tool-desc="' + esc(String(desc).toLowerCase()) + '" style="padding:8px 10px;border-bottom:1px solid var(--border-subtle);font-size:12px;display:flex;align-items:center;gap:6px;flex-wrap:wrap">'
25433
+ + '<code style="background:var(--bg-tertiary);padding:1px 6px;border-radius:3px;color:var(--accent);font-size:11px">' + esc(name) + '</code>'
25434
+ + typeBadge + apiBadge + liveBadge + connBadge
25435
+ + (desc ? '<span style="color:var(--text-secondary);flex:1;min-width:0">' + esc(String(desc).slice(0, 240)) + '</span>' : '')
25436
+ + '</div>';
25437
+ };
25438
+
25439
+ // Stable order — system surfaces first, then APIs, then the giant Composio.
25440
+ var orderedKeys = ['Core SDK', 'CLI Tools', 'Memory & Vault', 'Notes & Tasks', 'API Integrations',
25441
+ 'Goals & Workflows', 'Agent Management', 'Team', 'System', 'Global MCP Servers',
25442
+ 'Local Projects', 'Composio Toolkits'];
25443
+ var orderedCats = orderedKeys
25444
+ .map(function(k){ return categoryEntries.find(function(c){ return c.name === k; }); })
25445
+ .filter(function(c){ return c && c.items.length > 0; });
25446
+ // Append any unknown categories the server returned that aren't in our preferred order.
25447
+ for (var ci = 0; ci < categoryEntries.length; ci++) {
25448
+ if (orderedKeys.indexOf(categoryEntries[ci].name) === -1 && categoryEntries[ci].items.length > 0) {
25449
+ orderedCats.push(categoryEntries[ci]);
25450
+ }
25451
+ }
25452
+
25453
+ for (var c = 0; c < orderedCats.length; c++) {
25454
+ var cat = orderedCats[c];
25455
+ var openByDefault = cat.name !== 'Composio Toolkits' && cat.items.length <= 50;
25456
+ html += '<details class="toolsmcp-cat" data-cat-name="' + esc(cat.name.toLowerCase()) + '"' + (openByDefault ? ' open' : '') + ' style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;margin-bottom:10px;overflow:hidden">';
25457
+ html += '<summary style="padding:10px 14px;cursor:pointer;display:flex;align-items:center;gap:10px;font-size:13px;font-weight:600;color:var(--text-primary);user-select:none">'
25458
+ + '<span style="flex:1">' + esc(cat.name) + '</span>'
25459
+ + '<span style="font-size:11px;color:var(--text-muted);font-weight:500">' + cat.items.length + ' tool' + (cat.items.length === 1 ? '' : 's') + '</span>'
25460
+ + '</summary>';
25461
+ html += '<div class="toolsmcp-cat-body" style="background:var(--bg-primary);max-height:480px;overflow-y:auto">';
25462
+ // For Composio specifically, show first 200 + "show all" — 1037 tools at once is heavy DOM
25463
+ var items = cat.name === 'Composio Toolkits' ? cat.items.slice(0, 200) : cat.items;
25464
+ for (var ii = 0; ii < items.length; ii++) html += compositeRender(items[ii]);
25465
+ if (cat.name === 'Composio Toolkits' && cat.items.length > items.length) {
25466
+ html += '<div style="padding:10px 14px;text-align:center;font-size:11px;color:var(--text-muted);background:var(--bg-secondary);border-top:1px solid var(--border-subtle)">'
25467
+ + 'Showing ' + items.length + ' of ' + cat.items.length + ' Composio toolkits. Use the search box to find a specific service.'
25468
+ + '</div>';
25469
+ }
25470
+ html += '</div></details>';
25471
+ }
25472
+
25473
+ // ── MCP server transport panel — kept for connection status, reconnect, edit ─────
25474
+ if (servers.length > 0) {
25475
+ html += '<div style="margin-top:24px">';
25765
25476
  html += '<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:10px">'
25766
- + '<h3 style="margin:0;font-size:14px;font-weight:600;color:var(--text-primary)">' + esc(sec.label) + '</h3>'
25767
- + '<span style="font-size:11px;color:var(--text-muted);font-weight:500">' + bucket.length + '</span>'
25477
+ + '<h3 style="margin:0;font-size:14px;font-weight:600;color:var(--text-primary)">MCP servers</h3>'
25478
+ + '<span style="font-size:11px;color:var(--text-muted);font-weight:500">' + servers.length + '</span>'
25768
25479
  + '</div>';
25769
- html += '<div style="font-size:11px;color:var(--text-muted);margin-bottom:12px">' + esc(sec.desc) + '</div>';
25770
- if (bucket.length === 0) {
25771
- html += '<div class="empty-state" style="padding:14px;color:var(--text-muted);font-size:12px;background:var(--bg-secondary);border:1px dashed var(--border);border-radius:6px">No servers in this bucket.</div>';
25772
- } else {
25773
- html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px">';
25480
+ html += '<div style="font-size:11px;color:var(--text-muted);margin-bottom:12px">Configured MCP servers and their live connection status. Use Reconnect to clear cached errors; Edit to view command, args, env.</div>';
25481
+ // Bucket by transport so the page reads cleanly. Built-in / Custom buckets
25482
+ // are kept (some installs DO have clementine-tools or kernel) but absent
25483
+ // ones don't render an empty card grid.
25484
+ var buckets = { builtin: [], shell: [], external: [] };
25485
+ for (var i = 0; i < servers.length; i++) {
25486
+ var s = servers[i];
25487
+ var sname = s.name || '';
25488
+ var stype = s.type || 'stdio';
25489
+ var scmd = s.command || '';
25490
+ var kind;
25491
+ if (sname === 'clementine-tools' || sname === 'kernel') kind = 'builtin';
25492
+ else if (/^(sf|gh|gcloud|kubectl|docker|aws|az|terraform)$/.test(scmd) || /\\b(sf|gh|gcloud|kubectl)$/.test(scmd)) kind = 'shell';
25493
+ else kind = 'external';
25494
+ buckets[kind].push(s);
25495
+ }
25496
+ var bucketLabels = [
25497
+ { key: 'builtin', label: 'Built-in / In-process' },
25498
+ { key: 'shell', label: 'Shell wrappers' },
25499
+ { key: 'external', label: 'External (stdio / sse / http)' },
25500
+ ];
25501
+ for (var bk = 0; bk < bucketLabels.length; bk++) {
25502
+ var bucket = buckets[bucketLabels[bk].key] || [];
25503
+ if (bucket.length === 0) continue;
25504
+ html += '<div style="margin-bottom:18px">'
25505
+ + '<div style="font-size:12px;font-weight:500;color:var(--text-secondary);margin-bottom:8px">' + esc(bucketLabels[bk].label) + ' <span style="color:var(--text-muted);font-weight:400">· ' + bucket.length + '</span></div>'
25506
+ + '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px">';
25774
25507
  for (var b = 0; b < bucket.length; b++) html += renderMcpCatalogCard(bucket[b], statusMap);
25775
- html += '</div>';
25508
+ html += '</div></div>';
25776
25509
  }
25777
25510
  html += '</div>';
25778
25511
  }
25512
+
25779
25513
  panel.innerHTML = html;
25780
25514
  }
25781
25515
 
25516
+ // Filter tools by the search box at the top of the catalog. Lightweight
25517
+ // client-side filter — hides individual tool rows + collapses categories
25518
+ // that have zero matches, so 1k+ Composio entries don't slow typing.
25519
+ function filterToolsCatalog() {
25520
+ var input = document.getElementById('toolsmcp-search');
25521
+ if (!input) return;
25522
+ var q = (input.value || '').toLowerCase().trim();
25523
+ var cats = document.querySelectorAll('.toolsmcp-cat');
25524
+ for (var ci = 0; ci < cats.length; ci++) {
25525
+ var cat = cats[ci];
25526
+ var rows = cat.querySelectorAll('.toolsmcp-tool');
25527
+ var anyMatch = false;
25528
+ for (var ri = 0; ri < rows.length; ri++) {
25529
+ var name = rows[ri].getAttribute('data-tool-name') || '';
25530
+ var desc = rows[ri].getAttribute('data-tool-desc') || '';
25531
+ var match = !q || name.indexOf(q) !== -1 || desc.indexOf(q) !== -1;
25532
+ rows[ri].style.display = match ? '' : 'none';
25533
+ if (match) anyMatch = true;
25534
+ }
25535
+ cat.style.display = anyMatch || !q ? '' : 'none';
25536
+ // Auto-open categories with hits when the user is actively searching.
25537
+ if (q && anyMatch) cat.setAttribute('open', '');
25538
+ }
25539
+ }
25540
+
25541
+ // POST /api/tools/probe — runs probeAvailableTools(true) which boots a one-
25542
+ // shot SDK query to capture every tool currently surfaced. Updates the cache
25543
+ // at ~/.clementine/.tool-inventory.json. Then reloads the catalog so the
25544
+ // "LIVE" badges and probed-at timestamp reflect the new data.
25545
+ async function probeToolsMcpInventory() {
25546
+ var btn = document.getElementById('toolsmcp-probe-btn');
25547
+ var originalText = btn ? btn.textContent : '';
25548
+ if (btn) { btn.disabled = true; btn.textContent = 'Probing… (~5s)'; }
25549
+ try {
25550
+ var r = await apiFetch('/api/tools/probe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ force: true }) });
25551
+ var d = await r.json();
25552
+ if (d && d.ok) {
25553
+ toast('Probed ' + d.toolCount + ' tools', 'success');
25554
+ } else {
25555
+ toast(d && d.error ? 'Probe failed: ' + d.error : 'Probe failed', 'error');
25556
+ }
25557
+ } catch (e) {
25558
+ toast('Probe failed: ' + e, 'error');
25559
+ } finally {
25560
+ if (btn) { btn.disabled = false; btn.textContent = originalText || 'Refresh inventory'; }
25561
+ refreshToolsMcpCatalog();
25562
+ }
25563
+ }
25564
+
25782
25565
  // Render one MCP server card. Status pill colors mirror the PRD's five
25783
25566
  // states (connected / failed / needs-auth / pending / disabled). The
25784
25567
  // statusMap shape comes from gw.getMcpStatus() — varies a bit between
@@ -26035,7 +25818,6 @@ async function refreshCron() {
26035
25818
  // Reliability (failures stacked by category). Filled in by
26036
25819
  // refreshMiniDashboards from the same /api/cron/runs payload.
26037
25820
  html += '<div id="mini-dashboards" class="mini-dashboards"></div>';
26038
- html += renderOperationsSummary(ops);
26039
25821
 
26040
25822
  // ── Zone 1 — Running now (promoted to top, primary "what's live" view) ──
26041
25823
  if (visibleRunning.length > 0) {
@@ -29771,28 +29553,6 @@ function setScheduleFromCron(expr) {
29771
29553
  updateScheduleFromBuilder();
29772
29554
 
29773
29555
  // ── 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
29556
 
29797
29557
  // ── Activity Feed ─────────────────────────
29798
29558
  var activityLastTimestamp = '';
@@ -34987,132 +34747,6 @@ function briefingNeedsReviewClick(href) {
34987
34747
  }
34988
34748
  }
34989
34749
 
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
34750
 
35117
34751
  function timeUntil(iso) {
35118
34752
  if (!iso) return '';
@@ -35134,7 +34768,6 @@ async function refreshAll() {
35134
34768
  else refreshActivity(); // Fall back to direct /api/activity fetch when init didn't include it
35135
34769
  if (d.office) refreshTeamNav(d.office);
35136
34770
  // Home rail data — fire and forget, doesn't block init render.
35137
- if (currentPage === 'home') refreshHomeRail();
35138
34771
  if (d.version) {
35139
34772
  if (d.version.needsRestart && !_restartBannerShown) {
35140
34773
  _restartBannerShown = true;
@@ -38297,34 +37930,6 @@ async function refreshHomeMetrics() {
38297
37930
  }
38298
37931
 
38299
37932
  // ── 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
37933
 
38329
37934
  // ── Execution Analytics ───────────────────
38330
37935
  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.114",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",