clementine-agent 1.18.111 → 1.18.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -279,6 +279,13 @@ export async function buildSkillContext(jobName, jobPrompt, agentSlug, pinnedSki
|
|
|
279
279
|
}
|
|
280
280
|
if (prepared.length === 0)
|
|
281
281
|
return { text: '', applied, missing };
|
|
282
|
+
// Folder-form bundled-file budget. Anthropic skill spec says the body
|
|
283
|
+
// should be ≤500 lines; bundled files (templates/, reference docs)
|
|
284
|
+
// load on top. We cap aggregate inlined bundle bytes so a skill with a
|
|
285
|
+
// huge templates/ tree doesn't blow the context window — anything over
|
|
286
|
+
// the cap is left on disk and the LLM can Read it via the cron's cwd.
|
|
287
|
+
const BUNDLE_FILE_CAP = 5;
|
|
288
|
+
const BUNDLE_BYTES_CAP = 12000;
|
|
282
289
|
const skillLines = prepared.map(s => {
|
|
283
290
|
recordSkillUse(s.name);
|
|
284
291
|
memoryStore?.logSkillUse?.({
|
|
@@ -292,7 +299,62 @@ export async function buildSkillContext(jobName, jobPrompt, agentSlug, pinnedSki
|
|
|
292
299
|
let block = `### ${s.title}${s.source === 'pinned' ? ' _(pinned)_' : ''}\n${s.content}`;
|
|
293
300
|
if (s.toolsUsed.length > 0)
|
|
294
301
|
block += `\n**Tools:** ${s.toolsUsed.join(', ')}`;
|
|
295
|
-
|
|
302
|
+
// Folder-form skills (post-migration default): inline sibling .md
|
|
303
|
+
// files (templates/intro.md, reference.md, etc.) so the cron prompt
|
|
304
|
+
// actually sees them. SKILL.md itself is the body above. scripts/
|
|
305
|
+
// and other non-.md assets stay on disk — the cron's cwd has Bash
|
|
306
|
+
// access to them via the runtime working directory.
|
|
307
|
+
const folderPath = path.join(s.skillDir, s.name);
|
|
308
|
+
const skillEntry = path.join(folderPath, 'SKILL.md');
|
|
309
|
+
if (fs.existsSync(skillEntry)) {
|
|
310
|
+
let bytesUsed = 0;
|
|
311
|
+
let filesUsed = 0;
|
|
312
|
+
const collectMd = (subDir, label) => {
|
|
313
|
+
if (filesUsed >= BUNDLE_FILE_CAP || bytesUsed >= BUNDLE_BYTES_CAP)
|
|
314
|
+
return;
|
|
315
|
+
let entries;
|
|
316
|
+
try {
|
|
317
|
+
entries = fs.readdirSync(subDir).sort();
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
for (const entry of entries) {
|
|
323
|
+
if (filesUsed >= BUNDLE_FILE_CAP || bytesUsed >= BUNDLE_BYTES_CAP)
|
|
324
|
+
break;
|
|
325
|
+
if (entry === 'SKILL.md')
|
|
326
|
+
continue;
|
|
327
|
+
if (!entry.endsWith('.md'))
|
|
328
|
+
continue;
|
|
329
|
+
const full = path.join(subDir, entry);
|
|
330
|
+
try {
|
|
331
|
+
const content = fs.readFileSync(full, 'utf-8');
|
|
332
|
+
const remaining = BUNDLE_BYTES_CAP - bytesUsed;
|
|
333
|
+
const slice = content.slice(0, remaining);
|
|
334
|
+
const labeled = label ? `${label}/${entry}` : entry;
|
|
335
|
+
block += `\n\n#### ${labeled}\n${slice}`;
|
|
336
|
+
bytesUsed += slice.length;
|
|
337
|
+
filesUsed++;
|
|
338
|
+
}
|
|
339
|
+
catch { /* skip unreadable */ }
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
// Top-level bundled .md files (reference.md, etc.)
|
|
343
|
+
collectMd(folderPath, '');
|
|
344
|
+
// Common sub-dirs: templates/, references/. One level deep only —
|
|
345
|
+
// we don't recurse to keep the budget predictable.
|
|
346
|
+
for (const sub of ['templates', 'references']) {
|
|
347
|
+
if (filesUsed >= BUNDLE_FILE_CAP || bytesUsed >= BUNDLE_BYTES_CAP)
|
|
348
|
+
break;
|
|
349
|
+
const subPath = path.join(folderPath, sub);
|
|
350
|
+
if (fs.existsSync(subPath) && fs.statSync(subPath).isDirectory()) {
|
|
351
|
+
collectMd(subPath, sub);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else if (s.attachments.length > 0) {
|
|
356
|
+
// Legacy flat form: attachments live under <skill>.files/ alongside
|
|
357
|
+
// the skill .md. Kept for backward compat with un-migrated skills.
|
|
296
358
|
const attDir = path.join(s.skillDir, s.name + '.files');
|
|
297
359
|
for (const attName of s.attachments.slice(0, 3)) {
|
|
298
360
|
const attPath = path.join(attDir, attName);
|
|
@@ -74,7 +74,10 @@ export declare function searchSkills(query: string, limit?: number, agentSlug?:
|
|
|
74
74
|
export declare function loadSkillByName(name: string, agentSlug?: string, opts?: {
|
|
75
75
|
suppressedNames?: Set<string>;
|
|
76
76
|
}): SkillMatch | null;
|
|
77
|
-
/** Record that a skill was used (bump use count).
|
|
77
|
+
/** Record that a skill was used (bump use count). Handles both flat and
|
|
78
|
+
* folder-form skills. For folder form the counter lives under
|
|
79
|
+
* `clementine.useCount` (Anthropic-canonical frontmatter keeps top-level
|
|
80
|
+
* reserved for `name`/`description`). */
|
|
78
81
|
export declare function recordSkillUse(skillName: string, agentSlug?: string): void;
|
|
79
82
|
/** List all active skills (global + all agent-scoped). */
|
|
80
83
|
export declare function listSkills(agentSlug?: string): Array<{
|
|
@@ -325,11 +325,39 @@ async function mergeSkill(assistant, existing, incoming) {
|
|
|
325
325
|
*/
|
|
326
326
|
const skillEmbeddingCache = new Map();
|
|
327
327
|
/**
|
|
328
|
-
*
|
|
328
|
+
* Compute the canonical skill slug from a relative path.
|
|
329
|
+
*
|
|
330
|
+
* Two layouts are supported:
|
|
331
|
+
* - Folder form (Anthropic spec): `<name>/SKILL.md` → `<name>`
|
|
332
|
+
* (sibling .md files in that folder are bundled docs, not standalone skills)
|
|
333
|
+
* - Flat form (legacy): `<name>.md` → `<name>`
|
|
334
|
+
* (auto-generated MCP skills like `auto/discord/send.md` → `auto-discord-send`)
|
|
335
|
+
*
|
|
336
|
+
* NOTE: this is the single source of truth for "what slug should this file be
|
|
337
|
+
* loaded under." Both walkSkillFiles and loadSkillByName route through it so
|
|
338
|
+
* the dedupe/naming stays consistent.
|
|
339
|
+
*/
|
|
340
|
+
function slugFromRel(relPath) {
|
|
341
|
+
// Folder form: <a>/<b>/SKILL.md → <a>-<b>
|
|
342
|
+
if (/(?:^|[\\/])SKILL\.md$/.test(relPath)) {
|
|
343
|
+
const parts = relPath.replace(/[\\/]SKILL\.md$/, '').split(/[\\/]/);
|
|
344
|
+
return parts.join('-');
|
|
345
|
+
}
|
|
346
|
+
// Flat form: any other .md file
|
|
347
|
+
return relPath.replace(/\.md$/, '').replace(/[\\/]/g, '-');
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Recursively list every skill file under `dir`. Returns absolute
|
|
329
351
|
* paths, relative paths (for dedupe/naming), and an `isAuto` flag set
|
|
330
|
-
* when the file lives under an `auto/` subtree.
|
|
331
|
-
*
|
|
332
|
-
*
|
|
352
|
+
* when the file lives under an `auto/` subtree.
|
|
353
|
+
*
|
|
354
|
+
* Folder-form skills (`<name>/SKILL.md`) emit ONLY the SKILL.md and do
|
|
355
|
+
* not recurse — sibling .md files (templates/, reference docs) and
|
|
356
|
+
* subdirectories (scripts/) are bundled assets, not standalone skills.
|
|
357
|
+
*
|
|
358
|
+
* Flat-form skills (`<name>.md`, including auto-generated MCP skills
|
|
359
|
+
* under `auto/<server>/<tool>.md`) keep the legacy behavior so existing
|
|
360
|
+
* unmigrated installs don't regress.
|
|
333
361
|
*/
|
|
334
362
|
function walkSkillFiles(root) {
|
|
335
363
|
const out = [];
|
|
@@ -341,6 +369,20 @@ function walkSkillFiles(root) {
|
|
|
341
369
|
catch {
|
|
342
370
|
return;
|
|
343
371
|
}
|
|
372
|
+
// Folder-form short-circuit: a sub-directory with a SKILL.md is a bundled
|
|
373
|
+
// skill folder. Emit just the SKILL.md and skip everything else inside —
|
|
374
|
+
// bundled docs/scripts are not separate skills.
|
|
375
|
+
if (rel) {
|
|
376
|
+
const skillEntry = entries.find(e => e.isFile() && e.name === 'SKILL.md');
|
|
377
|
+
if (skillEntry) {
|
|
378
|
+
out.push({
|
|
379
|
+
filePath: path.join(dir, skillEntry.name),
|
|
380
|
+
relPath: path.join(rel, skillEntry.name),
|
|
381
|
+
isAuto: rel.split(path.sep)[0] === 'auto',
|
|
382
|
+
});
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
344
386
|
for (const ent of entries) {
|
|
345
387
|
const name = ent.name;
|
|
346
388
|
// Skip backup files and hidden files
|
|
@@ -401,9 +443,10 @@ export function searchSkills(query, limit = 3, agentSlug, opts) {
|
|
|
401
443
|
// tiebreak even when both match the query.
|
|
402
444
|
const files = walkSkillFiles(dir);
|
|
403
445
|
for (const { filePath, relPath, isAuto } of files) {
|
|
404
|
-
// Use
|
|
405
|
-
//
|
|
406
|
-
|
|
446
|
+
// Use slugFromRel so folder-form skills (`<name>/SKILL.md`) get
|
|
447
|
+
// their canonical name `<name>` and not `<name>-SKILL`. Same dedupe
|
|
448
|
+
// contract: distinct files cannot share a name.
|
|
449
|
+
const name = slugFromRel(relPath);
|
|
407
450
|
if (seen.has(name))
|
|
408
451
|
continue;
|
|
409
452
|
seen.add(name);
|
|
@@ -521,18 +564,61 @@ export function loadSkillByName(name, agentSlug, opts) {
|
|
|
521
564
|
if (opts?.suppressedNames?.has(name))
|
|
522
565
|
return null;
|
|
523
566
|
for (const dir of dirs) {
|
|
567
|
+
// Fast path: folder form. `<dir>/<name>/SKILL.md` is the canonical
|
|
568
|
+
// Anthropic layout post-migration. Try it first so a slug like
|
|
569
|
+
// `morning-briefing` resolves to `morning-briefing/SKILL.md`.
|
|
570
|
+
const folderEntry = path.join(dir, name, 'SKILL.md');
|
|
571
|
+
if (existsSync(folderEntry)) {
|
|
572
|
+
try {
|
|
573
|
+
const raw = readFileSync(folderEntry, 'utf-8');
|
|
574
|
+
const parsed = matter(raw);
|
|
575
|
+
const fmTools = parsed.data?.clementine?.tools?.allow ?? parsed.data?.toolsUsed ?? [];
|
|
576
|
+
return {
|
|
577
|
+
name,
|
|
578
|
+
title: parsed.data.title ?? parsed.data.name ?? name,
|
|
579
|
+
content: parsed.content, // full body — the runtime cap is gone
|
|
580
|
+
score: 0,
|
|
581
|
+
toolsUsed: Array.isArray(fmTools) ? fmTools : [],
|
|
582
|
+
attachments: parsed.data.attachments ?? [],
|
|
583
|
+
skillDir: dir,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// Fast path: flat form. `<dir>/<name>.md` (legacy + auto-discovery).
|
|
591
|
+
const flatEntry = path.join(dir, `${name}.md`);
|
|
592
|
+
if (existsSync(flatEntry)) {
|
|
593
|
+
try {
|
|
594
|
+
const raw = readFileSync(flatEntry, 'utf-8');
|
|
595
|
+
const parsed = matter(raw);
|
|
596
|
+
return {
|
|
597
|
+
name,
|
|
598
|
+
title: parsed.data.title ?? name,
|
|
599
|
+
content: parsed.content, // full body — the runtime cap is gone
|
|
600
|
+
score: 0,
|
|
601
|
+
toolsUsed: parsed.data.toolsUsed ?? [],
|
|
602
|
+
attachments: parsed.data.attachments ?? [],
|
|
603
|
+
skillDir: dir,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// Fallback: walk for nested forms (e.g. `auto/discord/send` slug).
|
|
524
611
|
const files = walkSkillFiles(dir);
|
|
525
612
|
for (const { filePath, relPath } of files) {
|
|
526
|
-
|
|
527
|
-
if (slug !== name)
|
|
613
|
+
if (slugFromRel(relPath) !== name)
|
|
528
614
|
continue;
|
|
529
615
|
try {
|
|
530
616
|
const raw = readFileSync(filePath, 'utf-8');
|
|
531
617
|
const parsed = matter(raw);
|
|
532
618
|
return {
|
|
533
|
-
name
|
|
534
|
-
title: parsed.data.title ??
|
|
535
|
-
content: parsed.content
|
|
619
|
+
name,
|
|
620
|
+
title: parsed.data.title ?? name,
|
|
621
|
+
content: parsed.content, // full body — the runtime cap is gone
|
|
536
622
|
score: 0,
|
|
537
623
|
toolsUsed: parsed.data.toolsUsed ?? [],
|
|
538
624
|
attachments: parsed.data.attachments ?? [],
|
|
@@ -546,21 +632,37 @@ export function loadSkillByName(name, agentSlug, opts) {
|
|
|
546
632
|
}
|
|
547
633
|
return null;
|
|
548
634
|
}
|
|
549
|
-
/** Record that a skill was used (bump use count).
|
|
635
|
+
/** Record that a skill was used (bump use count). Handles both flat and
|
|
636
|
+
* folder-form skills. For folder form the counter lives under
|
|
637
|
+
* `clementine.useCount` (Anthropic-canonical frontmatter keeps top-level
|
|
638
|
+
* reserved for `name`/`description`). */
|
|
550
639
|
export function recordSkillUse(skillName, agentSlug) {
|
|
551
640
|
try {
|
|
552
|
-
// Check agent dir first, then global
|
|
553
641
|
const dirs = agentSlug ? [agentSkillsDir(agentSlug), GLOBAL_SKILLS_DIR] : [GLOBAL_SKILLS_DIR];
|
|
554
642
|
for (const dir of dirs) {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
643
|
+
// Folder form: <dir>/<skillName>/SKILL.md
|
|
644
|
+
const folderEntry = path.join(dir, skillName, 'SKILL.md');
|
|
645
|
+
if (existsSync(folderEntry)) {
|
|
646
|
+
const raw = readFileSync(folderEntry, 'utf-8');
|
|
647
|
+
const parsed = matter(raw);
|
|
648
|
+
parsed.data.clementine = (parsed.data.clementine && typeof parsed.data.clementine === 'object')
|
|
649
|
+
? parsed.data.clementine
|
|
650
|
+
: {};
|
|
651
|
+
parsed.data.clementine.useCount = (parsed.data.clementine.useCount ?? 0) + 1;
|
|
652
|
+
parsed.data.clementine.lastUsed = new Date().toISOString();
|
|
653
|
+
writeFileSync(folderEntry, matter.stringify(parsed.content, parsed.data));
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
// Flat form: <dir>/<skillName>.md
|
|
657
|
+
const flatEntry = path.join(dir, `${skillName}.md`);
|
|
658
|
+
if (existsSync(flatEntry)) {
|
|
659
|
+
const raw = readFileSync(flatEntry, 'utf-8');
|
|
660
|
+
const parsed = matter(raw);
|
|
661
|
+
parsed.data.useCount = (parsed.data.useCount ?? 0) + 1;
|
|
662
|
+
parsed.data.lastUsed = new Date().toISOString();
|
|
663
|
+
writeFileSync(flatEntry, matter.stringify(parsed.content, parsed.data));
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
564
666
|
}
|
|
565
667
|
}
|
|
566
668
|
catch { /* non-fatal */ }
|
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
|
});
|
|
@@ -6846,17 +6703,9 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6846
6703
|
}
|
|
6847
6704
|
});
|
|
6848
6705
|
// ── CRON CRUD routes ──────────────────────────────────────────
|
|
6849
|
-
|
|
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
|
-
});
|
|
6706
|
+
// (Dead duplicate /api/projects handler removed in 1.18.113 — first
|
|
6707
|
+
// registration at line 6183 is the live one; Express ignores later
|
|
6708
|
+
// same-method same-path registrations.)
|
|
6860
6709
|
app.post('/api/projects/link', (req, res) => {
|
|
6861
6710
|
try {
|
|
6862
6711
|
const { path: projPath, description, keywords } = req.body;
|
|
@@ -10231,73 +10080,6 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
10231
10080
|
res.status(500).json({ error: String(err) });
|
|
10232
10081
|
}
|
|
10233
10082
|
});
|
|
10234
|
-
app.get('/api/skills', async (_req, res) => {
|
|
10235
|
-
try {
|
|
10236
|
-
const skillsDir = path.join(VAULT_DIR, '00-System', 'skills');
|
|
10237
|
-
if (!existsSync(skillsDir)) {
|
|
10238
|
-
res.json({ skills: [] });
|
|
10239
|
-
return;
|
|
10240
|
-
}
|
|
10241
|
-
// Aggregate last-7-day retrieval stats from skill_usage table (best-effort).
|
|
10242
|
-
const usageStats = new Map();
|
|
10243
|
-
if (existsSync(MEMORY_DB_PATH)) {
|
|
10244
|
-
try {
|
|
10245
|
-
const Database = (await import('better-sqlite3')).default;
|
|
10246
|
-
const db = new Database(MEMORY_DB_PATH, { readonly: true });
|
|
10247
|
-
try {
|
|
10248
|
-
const rows = db.prepare(`SELECT skill_name,
|
|
10249
|
-
COUNT(*) AS retrievals,
|
|
10250
|
-
MAX(retrieved_at) AS last_retrieved_at,
|
|
10251
|
-
AVG(score) AS avg_score
|
|
10252
|
-
FROM skill_usage
|
|
10253
|
-
WHERE retrieved_at >= datetime('now', '-7 days')
|
|
10254
|
-
GROUP BY skill_name`).all();
|
|
10255
|
-
for (const r of rows) {
|
|
10256
|
-
usageStats.set(r.skill_name, {
|
|
10257
|
-
retrievals7d: r.retrievals,
|
|
10258
|
-
lastRetrievedAt: r.last_retrieved_at,
|
|
10259
|
-
avgScore: r.avg_score,
|
|
10260
|
-
});
|
|
10261
|
-
}
|
|
10262
|
-
}
|
|
10263
|
-
catch { /* skill_usage may not exist on older DBs */ }
|
|
10264
|
-
db.close();
|
|
10265
|
-
}
|
|
10266
|
-
catch { /* non-fatal */ }
|
|
10267
|
-
}
|
|
10268
|
-
const files = readdirSync(skillsDir).filter(f => f.endsWith('.md'));
|
|
10269
|
-
const skills = files.map(f => {
|
|
10270
|
-
try {
|
|
10271
|
-
const parsed = matter(readFileSync(path.join(skillsDir, f), 'utf-8'));
|
|
10272
|
-
const name = f.replace('.md', '');
|
|
10273
|
-
const stats = usageStats.get(name);
|
|
10274
|
-
return {
|
|
10275
|
-
name,
|
|
10276
|
-
title: parsed.data.title ?? f,
|
|
10277
|
-
description: parsed.data.description ?? '',
|
|
10278
|
-
source: parsed.data.source ?? 'unknown',
|
|
10279
|
-
sourceJob: parsed.data.sourceJob ?? null,
|
|
10280
|
-
triggers: parsed.data.triggers ?? [],
|
|
10281
|
-
toolsUsed: parsed.data.toolsUsed ?? [],
|
|
10282
|
-
useCount: parsed.data.useCount ?? 0,
|
|
10283
|
-
lastUsed: parsed.data.lastUsed ?? null,
|
|
10284
|
-
createdAt: parsed.data.createdAt ?? '',
|
|
10285
|
-
updatedAt: parsed.data.updatedAt ?? '',
|
|
10286
|
-
retrievals7d: stats?.retrievals7d ?? 0,
|
|
10287
|
-
lastRetrievedAt: stats?.lastRetrievedAt ?? null,
|
|
10288
|
-
avgScore: stats?.avgScore ?? null,
|
|
10289
|
-
};
|
|
10290
|
-
}
|
|
10291
|
-
catch {
|
|
10292
|
-
return null;
|
|
10293
|
-
}
|
|
10294
|
-
}).filter(Boolean);
|
|
10295
|
-
res.json({ skills });
|
|
10296
|
-
}
|
|
10297
|
-
catch (err) {
|
|
10298
|
-
res.status(500).json({ error: String(err) });
|
|
10299
|
-
}
|
|
10300
|
-
});
|
|
10301
10083
|
app.post('/api/skills', (req, res) => {
|
|
10302
10084
|
try {
|
|
10303
10085
|
const { title, description, triggers, steps } = req.body;
|
|
@@ -10342,57 +10124,6 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
10342
10124
|
res.status(500).json({ error: String(err) });
|
|
10343
10125
|
}
|
|
10344
10126
|
});
|
|
10345
|
-
app.get('/api/skills/:name', (req, res) => {
|
|
10346
|
-
try {
|
|
10347
|
-
// Check agent-scoped first (via query param), then global
|
|
10348
|
-
const agentSlug = req.query.agent;
|
|
10349
|
-
let filePath;
|
|
10350
|
-
let skillDir;
|
|
10351
|
-
if (agentSlug) {
|
|
10352
|
-
const agentPath = path.join(VAULT_DIR, '00-System', 'agents', agentSlug, 'skills', `${req.params.name}.md`);
|
|
10353
|
-
if (existsSync(agentPath)) {
|
|
10354
|
-
filePath = agentPath;
|
|
10355
|
-
skillDir = path.join(VAULT_DIR, '00-System', 'agents', agentSlug, 'skills');
|
|
10356
|
-
}
|
|
10357
|
-
else {
|
|
10358
|
-
filePath = path.join(VAULT_DIR, '00-System', 'skills', `${req.params.name}.md`);
|
|
10359
|
-
skillDir = path.join(VAULT_DIR, '00-System', 'skills');
|
|
10360
|
-
}
|
|
10361
|
-
}
|
|
10362
|
-
else {
|
|
10363
|
-
filePath = path.join(VAULT_DIR, '00-System', 'skills', `${req.params.name}.md`);
|
|
10364
|
-
skillDir = path.join(VAULT_DIR, '00-System', 'skills');
|
|
10365
|
-
}
|
|
10366
|
-
if (!existsSync(filePath)) {
|
|
10367
|
-
res.status(404).json({ error: 'Skill not found' });
|
|
10368
|
-
return;
|
|
10369
|
-
}
|
|
10370
|
-
const matterMod = require('gray-matter');
|
|
10371
|
-
const parsed = matterMod(readFileSync(filePath, 'utf-8'));
|
|
10372
|
-
// Extract steps from content (after "## Procedure" heading)
|
|
10373
|
-
const procMatch = parsed.content.match(/## Procedure\s*\n([\s\S]*)/);
|
|
10374
|
-
const steps = procMatch ? procMatch[1].trim() : parsed.content.trim();
|
|
10375
|
-
// Load attachment file list with base64 content for builder reload
|
|
10376
|
-
const attachments = [];
|
|
10377
|
-
const filesDir = path.join(skillDir, `${req.params.name}.files`);
|
|
10378
|
-
if (existsSync(filesDir)) {
|
|
10379
|
-
for (const f of readdirSync(filesDir)) {
|
|
10380
|
-
try {
|
|
10381
|
-
const fp = path.join(filesDir, f);
|
|
10382
|
-
const stat = statSync(fp);
|
|
10383
|
-
if (stat.isFile() && stat.size < 10 * 1024 * 1024) {
|
|
10384
|
-
attachments.push({ filename: f, content: readFileSync(fp).toString('base64'), size: stat.size });
|
|
10385
|
-
}
|
|
10386
|
-
}
|
|
10387
|
-
catch { /* skip */ }
|
|
10388
|
-
}
|
|
10389
|
-
}
|
|
10390
|
-
res.json({ ...parsed.data, name: req.params.name, content: parsed.content, steps, attachmentFiles: attachments });
|
|
10391
|
-
}
|
|
10392
|
-
catch (err) {
|
|
10393
|
-
res.status(500).json({ error: String(err) });
|
|
10394
|
-
}
|
|
10395
|
-
});
|
|
10396
10127
|
// ── Agent-scoped Skills ──
|
|
10397
10128
|
app.get('/api/agents/:slug/skills', (req, res) => {
|
|
10398
10129
|
try {
|
|
@@ -13602,123 +13333,6 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
13602
13333
|
}
|
|
13603
13334
|
|
|
13604
13335
|
/* Right rail */
|
|
13605
|
-
.home-rail {
|
|
13606
|
-
display: flex;
|
|
13607
|
-
flex-direction: column;
|
|
13608
|
-
gap: 8px;
|
|
13609
|
-
overflow-y: auto;
|
|
13610
|
-
position: relative;
|
|
13611
|
-
}
|
|
13612
|
-
.home-rail.collapsed {
|
|
13613
|
-
display: none;
|
|
13614
|
-
}
|
|
13615
|
-
/* Auto-hide cards that have no actionable content (set via JS toggling .rail-card.empty) */
|
|
13616
|
-
.rail-card.empty { display: none; }
|
|
13617
|
-
.rail-collapse-btn {
|
|
13618
|
-
position: absolute;
|
|
13619
|
-
top: -4px;
|
|
13620
|
-
right: -4px;
|
|
13621
|
-
background: none;
|
|
13622
|
-
border: 1px solid var(--border);
|
|
13623
|
-
color: var(--text-muted);
|
|
13624
|
-
width: 22px;
|
|
13625
|
-
height: 22px;
|
|
13626
|
-
border-radius: 50%;
|
|
13627
|
-
cursor: pointer;
|
|
13628
|
-
font-size: 14px;
|
|
13629
|
-
display: none;
|
|
13630
|
-
align-items: center;
|
|
13631
|
-
justify-content: center;
|
|
13632
|
-
z-index: 5;
|
|
13633
|
-
}
|
|
13634
|
-
.rail-card {
|
|
13635
|
-
background: var(--bg-card);
|
|
13636
|
-
border: 1px solid var(--border);
|
|
13637
|
-
border-radius: var(--radius-md);
|
|
13638
|
-
overflow: hidden;
|
|
13639
|
-
box-shadow: var(--shadow-xs);
|
|
13640
|
-
}
|
|
13641
|
-
.rail-header {
|
|
13642
|
-
display: flex;
|
|
13643
|
-
align-items: center;
|
|
13644
|
-
justify-content: space-between;
|
|
13645
|
-
padding: 8px 12px;
|
|
13646
|
-
font-size: var(--text-xs);
|
|
13647
|
-
font-weight: 600;
|
|
13648
|
-
text-transform: uppercase;
|
|
13649
|
-
letter-spacing: 0.04em;
|
|
13650
|
-
color: var(--text-muted);
|
|
13651
|
-
background: transparent;
|
|
13652
|
-
}
|
|
13653
|
-
.rail-body { padding: 8px 12px 10px; font-size: var(--text-sm); line-height: 1.45; }
|
|
13654
|
-
.rail-body .empty-state, .rail-body .skel-row { font-size: var(--text-xs); }
|
|
13655
|
-
.rail-badge {
|
|
13656
|
-
display: inline-flex;
|
|
13657
|
-
align-items: center;
|
|
13658
|
-
justify-content: center;
|
|
13659
|
-
min-width: 18px;
|
|
13660
|
-
height: 18px;
|
|
13661
|
-
padding: 0 6px;
|
|
13662
|
-
border-radius: 9px;
|
|
13663
|
-
background: var(--clementine);
|
|
13664
|
-
color: #fff;
|
|
13665
|
-
font-size: 10px;
|
|
13666
|
-
font-weight: 600;
|
|
13667
|
-
}
|
|
13668
|
-
.rail-row {
|
|
13669
|
-
display: flex;
|
|
13670
|
-
align-items: center;
|
|
13671
|
-
gap: 8px;
|
|
13672
|
-
padding: 6px 0;
|
|
13673
|
-
font-size: 12px;
|
|
13674
|
-
border-bottom: 1px dashed var(--border);
|
|
13675
|
-
}
|
|
13676
|
-
.rail-row:last-child { border-bottom: none; }
|
|
13677
|
-
.rail-row .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
13678
|
-
.rail-row .meta { font-size: 10px; color: var(--text-muted); flex-shrink: 0; }
|
|
13679
|
-
|
|
13680
|
-
/* Floating "open rail" button when collapsed */
|
|
13681
|
-
.home-rail-toggle {
|
|
13682
|
-
position: fixed;
|
|
13683
|
-
top: 80px;
|
|
13684
|
-
right: 18px;
|
|
13685
|
-
z-index: 100;
|
|
13686
|
-
width: 36px;
|
|
13687
|
-
height: 36px;
|
|
13688
|
-
border-radius: 50%;
|
|
13689
|
-
background: var(--clementine);
|
|
13690
|
-
color: #fff;
|
|
13691
|
-
border: none;
|
|
13692
|
-
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
13693
|
-
cursor: pointer;
|
|
13694
|
-
font-size: 14px;
|
|
13695
|
-
display: none;
|
|
13696
|
-
}
|
|
13697
|
-
.home-rail.collapsed ~ .home-rail-toggle,
|
|
13698
|
-
.home-rail.collapsed + .home-rail-toggle { display: block; }
|
|
13699
|
-
|
|
13700
|
-
/* Narrow screens: rail becomes a slide-out drawer */
|
|
13701
|
-
@media (max-width: 1024px) {
|
|
13702
|
-
.home-layout { grid-template-columns: 1fr; }
|
|
13703
|
-
.home-rail {
|
|
13704
|
-
position: fixed;
|
|
13705
|
-
right: 0;
|
|
13706
|
-
top: var(--header-h);
|
|
13707
|
-
bottom: 0;
|
|
13708
|
-
width: 320px;
|
|
13709
|
-
max-width: 90vw;
|
|
13710
|
-
transform: translateX(100%);
|
|
13711
|
-
transition: transform 0.2s ease;
|
|
13712
|
-
background: var(--bg);
|
|
13713
|
-
border-left: 1px solid var(--border);
|
|
13714
|
-
box-shadow: -4px 0 20px rgba(0,0,0,0.15);
|
|
13715
|
-
padding: 14px;
|
|
13716
|
-
z-index: 50;
|
|
13717
|
-
}
|
|
13718
|
-
.home-rail.open { transform: translateX(0); }
|
|
13719
|
-
.rail-collapse-btn { display: flex; }
|
|
13720
|
-
.home-rail-toggle { display: block; }
|
|
13721
|
-
.home-rail.open ~ .home-rail-toggle { display: none; }
|
|
13722
13336
|
}
|
|
13723
13337
|
|
|
13724
13338
|
/* ── Cards ──────────────────────────────── */
|
|
@@ -22283,11 +21897,6 @@ function navigateTo(page, opts) {
|
|
|
22283
21897
|
if (t === 'chat') {
|
|
22284
21898
|
var ci = document.getElementById('chat-input');
|
|
22285
21899
|
if (ci) ci.focus();
|
|
22286
|
-
} else if (t === 'today') {
|
|
22287
|
-
var rail = document.getElementById('home-rail');
|
|
22288
|
-
if (rail && window.matchMedia('(max-width: 1024px)').matches) rail.classList.add('open');
|
|
22289
|
-
var p = document.getElementById('home-plan-content');
|
|
22290
|
-
if (p) p.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
22291
21900
|
} else if (t === 'activity') {
|
|
22292
21901
|
var act = document.getElementById('panel-activity');
|
|
22293
21902
|
if (act) act.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
@@ -24480,16 +24089,6 @@ function operationSectionHeader(title, subtitle, badgeClass, badgeText, marginTo
|
|
|
24480
24089
|
+ '</div>';
|
|
24481
24090
|
}
|
|
24482
24091
|
|
|
24483
|
-
function renderOperationsSummary(ops) {
|
|
24484
|
-
var s = ops.summary || {};
|
|
24485
|
-
return '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-bottom:16px">'
|
|
24486
|
-
+ '<div style="border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);padding:10px 12px"><div style="font-size:11px;color:var(--text-muted)">Needs Attention</div><div style="font-size:20px;font-weight:700;color:' + ((s.needsAttention || 0) > 0 ? 'var(--red)' : 'var(--green)') + '">' + esc(s.needsAttention || 0) + '</div></div>'
|
|
24487
|
-
+ '<div style="border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);padding:10px 12px"><div style="font-size:11px;color:var(--text-muted)">Scheduled Tasks</div><div style="font-size:20px;font-weight:700">' + esc(s.enabledScheduledTasks || 0) + '/' + esc(s.scheduledTasks || 0) + '</div></div>'
|
|
24488
|
-
+ '<div style="border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);padding:10px 12px"><div style="font-size:11px;color:var(--text-muted)">Scheduled Workflows</div><div style="font-size:20px;font-weight:700">' + esc(s.enabledScheduledWorkflows || 0) + '/' + esc(s.scheduledWorkflows || 0) + '</div></div>'
|
|
24489
|
-
+ '<div style="border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);padding:10px 12px"><div style="font-size:11px;color:var(--text-muted)">Running Now</div><div style="font-size:20px;font-weight:700;color:' + ((s.runningNow || 0) > 0 ? 'var(--blue)' : 'var(--text-primary)') + '">' + esc(s.runningNow || 0) + '</div></div>'
|
|
24490
|
-
+ '<div style="border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);padding:10px 12px"><div style="font-size:11px;color:var(--text-muted)">Scheduled Tokens</div><div style="font-size:20px;font-weight:700">' + esc(formatTokens(s.automationTokens || 0)) + '</div></div>'
|
|
24491
|
-
+ '</div>';
|
|
24492
|
-
}
|
|
24493
24092
|
|
|
24494
24093
|
function renderAttentionCard(item) {
|
|
24495
24094
|
var broken = item.brokenJob || null;
|
|
@@ -26035,7 +25634,6 @@ async function refreshCron() {
|
|
|
26035
25634
|
// Reliability (failures stacked by category). Filled in by
|
|
26036
25635
|
// refreshMiniDashboards from the same /api/cron/runs payload.
|
|
26037
25636
|
html += '<div id="mini-dashboards" class="mini-dashboards"></div>';
|
|
26038
|
-
html += renderOperationsSummary(ops);
|
|
26039
25637
|
|
|
26040
25638
|
// ── Zone 1 — Running now (promoted to top, primary "what's live" view) ──
|
|
26041
25639
|
if (visibleRunning.length > 0) {
|
|
@@ -29771,28 +29369,6 @@ function setScheduleFromCron(expr) {
|
|
|
29771
29369
|
updateScheduleFromBuilder();
|
|
29772
29370
|
|
|
29773
29371
|
// ── Timers ────────────────────────────────
|
|
29774
|
-
async function refreshTimers() {
|
|
29775
|
-
try {
|
|
29776
|
-
const r = await apiFetch('/api/timers');
|
|
29777
|
-
const d = await r.json();
|
|
29778
|
-
const count = Array.isArray(d) ? d.length : 0;
|
|
29779
|
-
var _tc = document.getElementById('nav-timer-count'); if (_tc) _tc.textContent = count;
|
|
29780
|
-
var _ttc = document.getElementById('tab-timer-count'); if (_ttc) { _ttc.textContent = count; _ttc.style.display = count > 0 ? '' : 'none'; }
|
|
29781
|
-
if (!Array.isArray(d) || d.length === 0) {
|
|
29782
|
-
document.getElementById('panel-timers').innerHTML = '<div class="empty-state">No pending timers</div>';
|
|
29783
|
-
return;
|
|
29784
|
-
}
|
|
29785
|
-
let html = '<table><tr><th>ID</th><th>Fires At</th><th>Message</th><th style="width:80px"></th></tr>';
|
|
29786
|
-
for (const t of d) {
|
|
29787
|
-
html += '<tr><td><code>' + esc(t.id || '?') + '</code></td>'
|
|
29788
|
-
+ '<td>' + esc(t.fireAt || t.fire_at || t.time || '') + '</td>'
|
|
29789
|
-
+ '<td>' + esc((t.message || t.prompt || '').slice(0, 100)) + '</td>'
|
|
29790
|
-
+ '<td><button class="btn-danger btn-sm" onclick="apiPost(\\x27/api/timers/' + encodeURIComponent(t.id) + '/cancel\\x27)">Cancel</button></td></tr>';
|
|
29791
|
-
}
|
|
29792
|
-
html += '</table>';
|
|
29793
|
-
document.getElementById('panel-timers').innerHTML = html;
|
|
29794
|
-
} catch(e) { }
|
|
29795
|
-
}
|
|
29796
29372
|
|
|
29797
29373
|
// ── Activity Feed ─────────────────────────
|
|
29798
29374
|
var activityLastTimestamp = '';
|
|
@@ -34987,132 +34563,6 @@ function briefingNeedsReviewClick(href) {
|
|
|
34987
34563
|
}
|
|
34988
34564
|
}
|
|
34989
34565
|
|
|
34990
|
-
function toggleHomeRail() {
|
|
34991
|
-
var rail = document.getElementById('home-rail');
|
|
34992
|
-
if (!rail) return;
|
|
34993
|
-
// Mobile: open/close. Desktop: collapse/show.
|
|
34994
|
-
if (window.matchMedia('(max-width: 1024px)').matches) {
|
|
34995
|
-
rail.classList.toggle('open');
|
|
34996
|
-
} else {
|
|
34997
|
-
rail.classList.toggle('collapsed');
|
|
34998
|
-
}
|
|
34999
|
-
}
|
|
35000
|
-
|
|
35001
|
-
function _railCard(bodyId) {
|
|
35002
|
-
var body = document.getElementById(bodyId);
|
|
35003
|
-
return body ? body.closest('.rail-card') : null;
|
|
35004
|
-
}
|
|
35005
|
-
function _setRailEmpty(bodyId, isEmpty) {
|
|
35006
|
-
var card = _railCard(bodyId);
|
|
35007
|
-
if (card) card.classList.toggle('empty', !!isEmpty);
|
|
35008
|
-
}
|
|
35009
|
-
|
|
35010
|
-
async function refreshHomeRail() {
|
|
35011
|
-
// Daemon status — only surface when explicitly stopped. Treat null/undefined
|
|
35012
|
-
// (running-state unknown) as "fine, hide" since the dashboard wouldn't be
|
|
35013
|
-
// serving requests if the daemon were truly down.
|
|
35014
|
-
try {
|
|
35015
|
-
var rs = await apiFetch('/api/status');
|
|
35016
|
-
var ds = await rs.json();
|
|
35017
|
-
var stopped = ds.running === false;
|
|
35018
|
-
var pip = document.querySelector('#rail-daemon-body .agent-activity-dot');
|
|
35019
|
-
var label = document.querySelector('#rail-daemon-body .agent-activity span:last-child');
|
|
35020
|
-
if (label) label.textContent = stopped ? 'Daemon stopped' : 'Running';
|
|
35021
|
-
if (pip) pip.style.background = stopped ? '#ef4444' : '#22c55e';
|
|
35022
|
-
var up = document.getElementById('rail-daemon-uptime');
|
|
35023
|
-
if (up && ds.uptimeMs) up.textContent = Math.round(ds.uptimeMs / 60000) + 'm';
|
|
35024
|
-
_setRailEmpty('rail-daemon-body', !stopped);
|
|
35025
|
-
} catch { _setRailEmpty('rail-daemon-body', true); }
|
|
35026
|
-
|
|
35027
|
-
// Today's plan (compact). Hide card if no plan or zero items.
|
|
35028
|
-
try {
|
|
35029
|
-
var rp = await apiFetch('/api/daily-plan');
|
|
35030
|
-
var dp = await rp.json();
|
|
35031
|
-
var planEl = document.getElementById('home-plan-content');
|
|
35032
|
-
var items = dp && dp.plan ? (dp.plan.items || []) : [];
|
|
35033
|
-
if (planEl) {
|
|
35034
|
-
if (items.length === 0) {
|
|
35035
|
-
planEl.innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No plan yet today.</div>';
|
|
35036
|
-
} else {
|
|
35037
|
-
planEl.innerHTML = items.slice(0, 4).map(function(it) {
|
|
35038
|
-
return '<div class="rail-row"><span class="label">' + esc(it.title || it.text || '') + '</span><span class="meta">' + esc(it.time || '') + '</span></div>';
|
|
35039
|
-
}).join('');
|
|
35040
|
-
}
|
|
35041
|
-
}
|
|
35042
|
-
_setRailEmpty('home-plan-content', items.length === 0);
|
|
35043
|
-
} catch {
|
|
35044
|
-
_setRailEmpty('home-plan-content', true);
|
|
35045
|
-
}
|
|
35046
|
-
|
|
35047
|
-
// Upcoming cron fires (next 3) — hide card if nothing scheduled
|
|
35048
|
-
try {
|
|
35049
|
-
var rc = await apiFetch('/api/cron');
|
|
35050
|
-
var dc = await rc.json();
|
|
35051
|
-
var jobs = (dc.jobs || []).filter(function(j) { return j.enabled && j.nextRun; });
|
|
35052
|
-
jobs.sort(function(a, b) { return new Date(a.nextRun).getTime() - new Date(b.nextRun).getTime(); });
|
|
35053
|
-
var top = jobs.slice(0, 3);
|
|
35054
|
-
var ue = document.getElementById('rail-upcoming');
|
|
35055
|
-
var uc = document.getElementById('rail-upcoming-count');
|
|
35056
|
-
if (uc) uc.textContent = String(jobs.length);
|
|
35057
|
-
if (ue) {
|
|
35058
|
-
ue.innerHTML = top.map(function(j) {
|
|
35059
|
-
return '<div class="rail-row clickable-row" onclick="navigateTo(\\x27build\\x27,{tab:\\x27crons\\x27})"><span class="label">' + esc(j.name) + '</span><span class="meta">' + esc(timeUntil(j.nextRun)) + '</span></div>';
|
|
35060
|
-
}).join('');
|
|
35061
|
-
}
|
|
35062
|
-
_setRailEmpty('rail-upcoming', top.length === 0);
|
|
35063
|
-
} catch { _setRailEmpty('rail-upcoming', true); }
|
|
35064
|
-
|
|
35065
|
-
// Active unleashed runs — hide card unless something running
|
|
35066
|
-
try {
|
|
35067
|
-
var ru = await apiFetch('/api/unleashed');
|
|
35068
|
-
var du = await ru.json();
|
|
35069
|
-
var active = (du.tasks || []).filter(function(t) { return t.live === true || t.runtimeState === 'active'; });
|
|
35070
|
-
var ae = document.getElementById('rail-active');
|
|
35071
|
-
var ac = document.getElementById('rail-active-count');
|
|
35072
|
-
if (ac) {
|
|
35073
|
-
if (active.length > 0) { ac.style.display = ''; ac.textContent = String(active.length); }
|
|
35074
|
-
else ac.style.display = 'none';
|
|
35075
|
-
}
|
|
35076
|
-
if (ae) {
|
|
35077
|
-
ae.innerHTML = active.map(function(t) {
|
|
35078
|
-
return '<div class="rail-row clickable-row" onclick="navigateTo(\\x27build\\x27,{tab:\\x27crons\\x27})"><span class="label">' + esc(t.name) + '</span><span class="meta">' + esc(t.phase || '') + '</span></div>';
|
|
35079
|
-
}).join('');
|
|
35080
|
-
}
|
|
35081
|
-
_setRailEmpty('rail-active', active.length === 0);
|
|
35082
|
-
} catch { _setRailEmpty('rail-active', true); }
|
|
35083
|
-
|
|
35084
|
-
// Time saved (compact). Hide if zero.
|
|
35085
|
-
try {
|
|
35086
|
-
var rm = await apiFetch('/api/metrics?period=week');
|
|
35087
|
-
var dm = await rm.json();
|
|
35088
|
-
var minutes = ((dm.cronRuns || 0) * 5) + ((dm.exchanges || 0) * 2);
|
|
35089
|
-
var ts = document.getElementById('rail-time-saved');
|
|
35090
|
-
if (ts) {
|
|
35091
|
-
if (minutes >= 60) ts.innerHTML = '<div style="font-size:var(--text-md);font-weight:600">' + (minutes / 60).toFixed(1) + 'h</div><div style="font-size:11px;color:var(--text-muted)">' + (dm.cronRuns || 0) + ' runs · ' + (dm.exchanges || 0) + ' chats</div>';
|
|
35092
|
-
else ts.innerHTML = '<div style="font-size:var(--text-md);font-weight:600">' + minutes + 'm</div><div style="font-size:11px;color:var(--text-muted)">' + (dm.cronRuns || 0) + ' runs</div>';
|
|
35093
|
-
}
|
|
35094
|
-
_setRailEmpty('rail-time-saved', minutes === 0);
|
|
35095
|
-
} catch { _setRailEmpty('rail-time-saved', true); }
|
|
35096
|
-
|
|
35097
|
-
// Approvals — hide card unless something pending
|
|
35098
|
-
try {
|
|
35099
|
-
var rsi = await apiFetch('/api/self-improve');
|
|
35100
|
-
var dsi = await rsi.json();
|
|
35101
|
-
var pending = (dsi.proposals || []).filter(function(p) { return p.status === 'pending'; });
|
|
35102
|
-
var ae2 = document.getElementById('rail-approvals');
|
|
35103
|
-
var ac2 = document.getElementById('rail-approvals-count');
|
|
35104
|
-
if (ac2) {
|
|
35105
|
-
if (pending.length > 0) { ac2.style.display = ''; ac2.textContent = String(pending.length); }
|
|
35106
|
-
else ac2.style.display = 'none';
|
|
35107
|
-
}
|
|
35108
|
-
if (ae2) {
|
|
35109
|
-
ae2.innerHTML = pending.slice(0, 3).map(function(p) {
|
|
35110
|
-
return '<div class="rail-row clickable-row" onclick="navigateTo(\\x27brain\\x27,{tab:\\x27learning\\x27})"><span class="label">' + esc(p.area || 'proposal') + ': ' + esc((p.target || '').slice(0, 40)) + '</span><span class="meta">' + esc(((p.score || 0) * 100).toFixed(0)) + '%</span></div>';
|
|
35111
|
-
}).join('');
|
|
35112
|
-
}
|
|
35113
|
-
_setRailEmpty('rail-approvals', pending.length === 0);
|
|
35114
|
-
} catch { _setRailEmpty('rail-approvals', true); }
|
|
35115
|
-
}
|
|
35116
34566
|
|
|
35117
34567
|
function timeUntil(iso) {
|
|
35118
34568
|
if (!iso) return '';
|
|
@@ -35134,7 +34584,6 @@ async function refreshAll() {
|
|
|
35134
34584
|
else refreshActivity(); // Fall back to direct /api/activity fetch when init didn't include it
|
|
35135
34585
|
if (d.office) refreshTeamNav(d.office);
|
|
35136
34586
|
// Home rail data — fire and forget, doesn't block init render.
|
|
35137
|
-
if (currentPage === 'home') refreshHomeRail();
|
|
35138
34587
|
if (d.version) {
|
|
35139
34588
|
if (d.version.needsRestart && !_restartBannerShown) {
|
|
35140
34589
|
_restartBannerShown = true;
|
|
@@ -38297,34 +37746,6 @@ async function refreshHomeMetrics() {
|
|
|
38297
37746
|
}
|
|
38298
37747
|
|
|
38299
37748
|
// ── Home Page: Sessions Tab ──────────────
|
|
38300
|
-
async function refreshHomeSessions() {
|
|
38301
|
-
var container = document.getElementById('panel-sessions-home');
|
|
38302
|
-
if (!container) return;
|
|
38303
|
-
try {
|
|
38304
|
-
var r = await apiFetch('/api/sessions');
|
|
38305
|
-
var d = await r.json();
|
|
38306
|
-
var keys = Object.keys(d);
|
|
38307
|
-
if (keys.length === 0) { container.innerHTML = '<div class="empty-state">No active sessions</div>'; return; }
|
|
38308
|
-
var html = '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px">';
|
|
38309
|
-
keys.forEach(function(key) {
|
|
38310
|
-
var s = d[key];
|
|
38311
|
-
var icon = key.indexOf('discord') >= 0 ? '💬' : 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
37749
|
|
|
38329
37750
|
// ── Execution Analytics ───────────────────
|
|
38330
37751
|
async function refreshAdvisorAnalytics() {
|