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.
- package/dist/cli/dashboard.js +242 -637
- package/package.json +1 -1
package/dist/cli/dashboard.js
CHANGED
|
@@ -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
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
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 & MCP catalog…</div>';
|
|
25703
|
-
|
|
25704
|
-
|
|
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
|
|
25707
|
-
|
|
25708
|
-
|
|
25709
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
25728
|
-
tabCount.style.display =
|
|
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
|
-
|
|
25752
|
-
|
|
25753
|
-
|
|
25754
|
-
|
|
25755
|
-
|
|
25756
|
-
|
|
25757
|
-
|
|
25758
|
-
|
|
25759
|
-
|
|
25760
|
-
|
|
25761
|
-
|
|
25762
|
-
|
|
25763
|
-
|
|
25764
|
-
|
|
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 & 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)">
|
|
25767
|
-
+ '<span style="font-size:11px;color:var(--text-muted);font-weight:500">' +
|
|
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">
|
|
25770
|
-
|
|
25771
|
-
|
|
25772
|
-
|
|
25773
|
-
|
|
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 ? '💬' : key.indexOf('slack') >= 0 ? '🗨' : key.indexOf('telegram') >= 0 ? '✈' : key.indexOf('dashboard') >= 0 ? '🌐' : '💬';
|
|
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() {
|