Skill: 0xraini/nightly-build (version 1.1.0)

# Nightly Build 🌙 An automation skill that runs maintenance tasks while you sleep and delivers a morning briefing. Inspired by [The Nightly Build](https://www.moltbook.com/post/562faad7-f9cc-49a3-8520-2bdf362606bb). ## Commands - `nightly report` — Show the last nightly build report. - `nightly run` — Trigger a manual run (for testing). - `nightly config` — Configure tasks (update skills, check disk, etc.). ## Tasks - 📦 **Skill Audit**: Run `npm audit` on installed skills. - 🔄 **Auto-Update**: Pull latest changes from git repos. - 🧹 **Cleanup**: Remove temporary files and old logs. - 📊 **Health Check**: Verify disk space and system load. - 📝 **Briefing**: Summarize everything into a morning report. ## Setup Add this to your cron (e.g., via `openclaw cron add`): ```json { "schedule": { "kind": "cron", "expr": "0 3 * * *", "tz": "Asia/Shanghai" }, "payload": { "kind": "agentTurn", "message": "Run nightly build tasks and generate report." } } ```

path: scripts/nightly.mjs
content: #!/usr/bin/env node

import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';

// --- CONFIG ---
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const WORKSPACE_DIR = process.env.WORKSPACE_DIR || path.resolve(__dirname, '../../..');
const SKILLS_DIR = path.join(WORKSPACE_DIR, 'skills');
const MEMORY_DIR = path.join(WORKSPACE_DIR, 'memory');
const REPORT_FILE = path.join(MEMORY_DIR, 'nightly-report.md');

// --- HELPERS ---
function log(msg) {
  const ts = new Date().toISOString();
  console.error(`[${ts}] ${msg}`);
}

function run(cmd, cwd = WORKSPACE_DIR) {
  try {
    return execSync(cmd, { cwd, encoding: 'utf-8', timeout: 60000 }).trim();
  } catch (err) {
    return `ERROR: ${err.message?.split('\n')[0] || 'unknown'}`;
  }
}

// --- TASKS ---

function taskSystemLoad() {
  let r = '### ⚡ System Status\n\n';
  const uptime = run('uptime');
  r += `- Uptime: \`${uptime}\`\n`;
  const mem = run("vm_stat | head -5 | tail -3");
  if (mem && !mem.startsWith('ERROR')) r += `- Memory:\n\`\`\`\n${mem}\n\`\`\`\n`;
  return r;
}

function taskDiskUsage() {
  let r = '### 💾 Disk Usage\n\n';
  const df = run('df -h / | tail -1');
  const parts = df.split(/\s+/);
  if (parts.length >= 5) {
    r += `- Total: ${parts[1]} | Used: ${parts[2]} (${parts[4]}) | Free: ${parts[3]}\n`;
    const pct = parseInt(parts[4]);
    if (pct > 90) r += `- ⚠️ **Disk usage above 90%!**\n`;
    else r += `- ✅ Disk healthy\n`;
  } else {
    r += `\`${df}\`\n`;
  }
  return r;
}

function taskSkillAudit() {
  let r = '### 📦 Skill Inventory\n\n';
  if (!fs.existsSync(SKILLS_DIR)) return r + 'No skills directory found.\n';

  const skills = fs.readdirSync(SKILLS_DIR).filter(s =>
    fs.statSync(path.join(SKILLS_DIR, s)).isDirectory()
  );

  r += `Found **${skills.length}** skills:\n`;
  for (const s of skills) {
    const hasSkillMd = fs.existsSync(path.join(SKILLS_DIR, s, 'SKILL.md'));
    const hasPkg = fs.existsSync(path.join(SKILLS_DIR, s, 'package.json'));
    const icon = hasSkillMd ? '✅' : '⚠️';
    const extra = hasPkg ? ' (npm)' : '';
    r += `- ${icon} \`${s}\`${extra}\n`;
  }
  return r;
}

function taskGitStatus() {
  let r = '### 🔄 Git Status\n\n';
  const status = run('git status --porcelain');
  if (!status || status.startsWith('ERROR')) {
    r += 'Not a git repo or git not available.\n';
    return r;
  }
  const lines = status.split('\n').filter(Boolean);
  if (lines.length === 0) {
    r += '- ✅ Working tree clean\n';
  } else {
    r += `- 📝 ${lines.length} uncommitted change(s)\n`;
    // Show up to 10
    for (const l of lines.slice(0, 10)) r += `  - \`${l.trim()}\`\n`;
    if (lines.length > 10) r += `  - ... and ${lines.length - 10} more\n`;
  }
  return r;
}

function taskLogCleanup() {
  let r = '### 🧹 Cleanup\n\n';
  let cleaned = 0;

  // Clean old log files (> 7 days)
  const memDir = MEMORY_DIR;
  if (fs.existsSync(memDir)) {
    const now = Date.now();
    const files = fs.readdirSync(memDir);
    for (const f of files) {
      if (f.endsWith('.log')) {
        const fp = path.join(memDir, f);
        const stat = fs.statSync(fp);
        const ageDays = (now - stat.mtimeMs) / 86400000;
        if (ageDays > 7) {
          r += `- 🗑️ Removed old log: \`${f}\` (${Math.floor(ageDays)}d old)\n`;
          fs.unlinkSync(fp);
          cleaned++;
        }
      }
    }
  }

  if (cleaned === 0) r += '- ✅ Nothing to clean\n';
  return r;
}

function taskMemorySize() {
  let r = '### 🧠 Memory Stats\n\n';
  if (!fs.existsSync(MEMORY_DIR)) return r + 'No memory directory.\n';

  const files = fs.readdirSync(MEMORY_DIR);
  let totalSize = 0;
  let mdCount = 0;

  for (const f of files) {
    const fp = path.join(MEMORY_DIR, f);
    const stat = fs.statSync(fp);
    totalSize += stat.size;
    if (f.endsWith('.md')) mdCount++;
  }

  r += `- ${mdCount} memory files, ${files.length} total\n`;
  r += `- Total size: ${(totalSize / 1024).toFixed(1)} KB\n`;

  // Check MEMORY.md size
  const memMd = path.join(WORKSPACE_DIR, 'MEMORY.md');
  if (fs.existsSync(memMd)) {
    const sz = fs.statSync(memMd).size;
    r += `- MEMORY.md: ${(sz / 1024).toFixed(1)} KB`;
    if (sz > 10240) r += ` ⚠️ (getting large, consider pruning)`;
    r += '\n';
  }

  return r;
}

// --- MAIN ---
function main() {
  log('🌙 Nightly Build starting...');

  if (!fs.existsSync(MEMORY_DIR)) fs.mkdirSync(MEMORY_DIR, { recursive: true });

  const now = new Date();
  const dateStr = now.toLocaleDateString('zh-CN', { timeZone: 'Asia/Shanghai' });
  const timeStr = now.toLocaleTimeString('zh-CN', { timeZone: 'Asia/Shanghai' });

  let report = `# 🌙 Nightly Build Report\n**${dateStr} ${timeStr}**\n\n`;

  report += taskSystemLoad() + '\n';
  report += taskDiskUsage() + '\n';
  report += taskSkillAudit() + '\n';
  report += taskGitStatus() + '\n';
  report += taskLogCleanup() + '\n';
  report += taskMemorySize() + '\n';

  report += '---\n*Generated by `nightly-build` skill* 🦞\n';

  fs.writeFileSync(REPORT_FILE, report);
  log(`Report saved to ${REPORT_FILE}`);

  // Output to stdout (agent reads this)
  console.log(report);
}

main();


path: scripts/nightly.ts
content: #!/usr/bin/env node

import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';

// --- CONFIG ---
const WORKSPACE_DIR = process.env.WORKSPACE_DIR || process.cwd();
const SKILLS_DIR = path.join(WORKSPACE_DIR, 'skills');
const MEMORY_DIR = path.join(WORKSPACE_DIR, 'memory');
const REPORT_FILE = path.join(MEMORY_DIR, 'nightly-report.md');
const LOG_FILE = path.join(MEMORY_DIR, 'nightly.log');

// --- HELPERS ---
function log(msg: string) {
  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] ${msg}`);
  fs.appendFileSync(LOG_FILE, `[${timestamp}] ${msg}\n`);
}

function runCmd(cmd: string, cwd: string = WORKSPACE_DIR): string {
  try {
    log(`Running: ${cmd} in ${cwd}`);
    const output = execSync(cmd, { cwd, encoding: 'utf-8', timeout: 60000 }); // 60s timeout
    return output.trim();
  } catch (err: any) {
    const errorMsg = `Command failed: ${cmd}\nError: ${err.message}`;
    log(errorMsg);
    return `ERROR: ${errorMsg}`;
  }
}

// --- TASKS ---

// 1. Skill Audit (npm audit)
function auditSkills(): string {
  let report = "### 📦 Skill Audit\n\n";
  if (!fs.existsSync(SKILLS_DIR)) {
    return report + "No skills directory found.\n";
  }

  const skills = fs.readdirSync(SKILLS_DIR);
  let vulnerableCount = 0;

  for (const skill of skills) {
    const skillPath = path.join(SKILLS_DIR, skill);
    if (fs.existsSync(path.join(skillPath, 'package.json'))) {
      try {
        // Just check, don't fix automatically yet
        runCmd('npm audit --audit-level=high', skillPath);
        report += `- ✅ ${skill}: Secure\n`;
      } catch (e) {
        report += `- ⚠️ ${skill}: High vulnerabilities found!\n`;
        vulnerableCount++;
      }
    }
  }
  
  if (vulnerableCount === 0) report += "\nAll skills are clean.\n";
  return report;
}

// 2. Git Updates
function checkGitUpdates(): string {
  let report = "### 🔄 Git Updates\n\n";
  
  // Check workspace root
  try {
    const status = runCmd('git remote update && git status -uno');
    if (status.includes('Your branch is behind')) {
      report += `- ⬇️ Workspace: Update available (run 'git pull')\n`;
    } else {
      report += `- ✅ Workspace: Up to date\n`;
    }
  } catch (e) {
    report += `- ❓ Workspace: Not a git repo or check failed\n`;
  }

  return report;
}

// 3. Disk Usage
function checkDisk(): string {
  let report = "### 💾 Disk Usage\n\n";
  try {
    const output = runCmd('df -h . | tail -1');
    report += `\`${output}\`\n`;
  } catch (e) {
    report += "Could not check disk usage.\n";
  }
  return report;
}

// 4. System Load
function checkLoad(): string {
  let report = "### ⚡ System Load\n\n";
  try {
    const uptime = runCmd('uptime');
    report += `\`${uptime}\`\n`;
  } catch (e) {
    report += "Could not check uptime.\n";
  }
  return report;
}

// --- MAIN ---
function main() {
  log("Starting Nightly Build...");
  
  const date = new Date().toLocaleDateString();
  let finalReport = `# 🌙 Nightly Build Report (${date})\n\n`;

  finalReport += checkLoad() + "\n";
  finalReport += checkDisk() + "\n";
  finalReport += checkGitUpdates() + "\n";
  finalReport += auditSkills() + "\n";

  finalReport += "\n---\n*Generated by Nightly Build Skill* 🦞";

  fs.writeFileSync(REPORT_FILE, finalReport);
  log(`Report generated at ${REPORT_FILE}`);
  
  console.log(finalReport); // Output to stdout for agent to read
}

main();


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)