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