#!/usr/bin/env node
/**
* HERMES SKILL SELF-INSTALLER: face-detection
*
* Hermes can run this file autonomously to install the face detection skill.
* It handles: directory setup, npm install, model downloads, skill file creation,
* and registry registration โ with no human input required.
*
* Usage (Hermes runs this):
* node install-face-detection.js [--skills-dir /path/to/skills] [--dry-run]
*/
ย
const { execSync, exec } = require('child_process');
const https = require('https');
const fs = require('fs');
const path = require('path');
ย
// โโโ CONFIG โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ย
const args = process.argv.slice(2);
const DRY_RUN = args.includes('--dry-run');
const SKILLS_DIR = (() => {
const idx = args.indexOf('--skills-dir');
if (idx !== -1) return args[idx + 1];
// Auto-detect common Hermes skill directory locations
const candidates = [
path.join(process.env.HOME, 'hermes-agent', 'skills'),
path.join(process.env.HOME, 'hermes', 'skills'),
path.join(__dirname, '..', 'skills'),
path.join(__dirname, 'skills'),
];
for (const c of candidates) {
if (fs.existsSync(c)) return c;
}
// Default: create next to this installer
return path.join(__dirname, 'skills');
})();
ย
const SKILL_DIR = path.join(SKILLS_DIR, 'face-detection');
const MODELS_DIR = path.join(SKILL_DIR, 'models');
const REGISTRY = path.join(SKILLS_DIR, 'skills-registry.json');
ย
const MODEL_BASE_URL =
'
https://raw.githubusercontent.com/justadudewhohacks/face-api.js/master/weights';ย
const REQUIRED_MODELS = [
'ssd_mobilenetv1_model-weights_manifest.json',
'ssd_mobilenetv1_model-shard1',
'ssd_mobilenetv1_model-shard2',
'face_landmark_68_model-weights_manifest.json',
'face_landmark_68_model-shard1',
'face_expression_model-weights_manifest.json',
'face_expression_model-shard1',
];
ย
const NPM_PACKAGES = ['face-api.js', 'canvas', '@tensorflow/tfjs-node'];
ย
// โโโ LOGGER โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ย
const log = {
info: (msg) => console.log(
[INFO] ${msg}),
ok: (msg) => console.log(
[OK] ${msg}),
skip: (msg) => console.log(
[SKIP] ${msg}),
warn: (msg) => console.warn(
[WARN] ${msg}),
error: (msg) => console.error(
[ERROR] ${msg}),
dry: (msg) => console.log(
[DRY] ${msg}),
};
ย
// โโโ HELPERS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ย
function sh(cmd, opts = {}) {
if (DRY_RUN) { log.dry(cmd); return ''; }
log.info(
$ ${cmd});
return execSync(cmd, { stdio: 'pipe', encoding: 'utf8', ...opts });
}
ย
function downloadFile(url, dest) {
return new Promise((resolve, reject) => {
if (DRY_RUN) { log.dry(
download ${url} โ ${dest}); return resolve(); }
if (fs.existsSync(dest)) { log.skip(
Already exists: ${path.basename(dest)}); return resolve(); }
log.info(
Downloading: ${path.basename(dest)});
const file = fs.createWriteStream(dest);
https.get(url, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
file.close();
fs.unlinkSync(dest);
return downloadFile(res.headers.location, dest).then(resolve).catch(reject);
}
if (res.statusCode !== 200) {
file.close();
fs.unlinkSync(dest);
return reject(new Error(
HTTP ${res.statusCode} for ${url}));
}
res.pipe(file);
file.on('finish', () => { file.close(); log.ok(
Downloaded: ${path.basename(dest)}); resolve(); });
}).on('error', (err) => { fs.unlinkSync(dest); reject(err); });
});
}
ย
function updateRegistry(skillMeta) {
let registry = [];
if (fs.existsSync(REGISTRY)) {
try { registry = JSON.parse(fs.readFileSync(REGISTRY, 'utf8')); } catch {}
}
const existing = registry.findIndex(s => s.name === skillMeta.name);
if (existing >= 0) {
registry[existing] = skillMeta;
log.info('Updated existing skill entry in registry.');
} else {
registry.push(skillMeta);
log.info('Added new skill entry to registry.');
}
if (!DRY_RUN) fs.writeFileSync(REGISTRY, JSON.stringify(registry, null, 2));
log.ok(
Registry updated: ${REGISTRY});
}
ย
// โโโ SKILL SOURCE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ย
const SKILL_INDEX_JS =
/**
* Hermes Skill: face-detection
* Auto-installed by install-face-detection.js
* Detects faces in images โ returns count, bounding boxes, expressions.
*/
ย
const faceapi = require('face-api.js');
const canvas = require('canvas');
const path = require('path');
const fs = require('fs');
ย
const { Canvas, Image, ImageData } = canvas;
faceapi.env.monkeyPatch({ Canvas, Image, ImageData });
ย
const MODELS_PATH = path.join(__dirname, 'models');
let modelsLoaded = false;
ย
async function loadModels() {
if (modelsLoaded) return;
await faceapi.nets.ssdMobilenetv1.loadFromDisk(MODELS_PATH);
await faceapi.nets.faceLandmark68Net.loadFromDisk(MODELS_PATH);
await faceapi.nets.faceExpressionNet.loadFromDisk(MODELS_PATH);
modelsLoaded = true;
}
ย
async function detectFaces(input) {
await loadModels();
ย
let img;
if (/^https?:\\/\\//.test(input)) {
img = await canvas.loadImage(input);
} else if (fs.existsSync(input)) {
img = await canvas.loadImage(input);
} else {
throw new Error(\Input not found or invalid: \${input}\
);
}
ย
const detections = await faceapi
.detectAllFaces(img)
.withFaceLandmarks()
.withFaceExpressions();
ย
if (detections.length === 0) {
return { faceCount: 0, faces: [], message: 'No faces detected.' };
}
ย
const faces = detections.map((det, i) => {
const { x, y, width, height } = det.detection.box;
const dominant = Object.entries(det.expressions).sort((a, b) => b[1] - a[1])[0];
return {
id: i + 1,
confidence: +det.detection.score.toFixed(3),
boundingBox: {
x: Math.round(x), y: Math.round(y),
width: Math.round(width), height: Math.round(height)
},
dominantExpression: { label: dominant[0], score: +dominant[1].toFixed(3) },
allExpressions: Object.fromEntries(
Object.entries(det.expressions).map(([k, v]) => [k, +v.toFixed(3)])
),
};
});
ย
return {
faceCount: faces.length,
imageSize: { width: img.width, height: img.height },
faces,
};
}
ย
module.exports = {
name: 'face-detection',
version: '1.0.0',
description: 'Detect faces in a local image file or public URL. Returns count, bounding boxes, confidence scores, and facial expressions.',
ย
triggers: [
'detect faces',
'how many faces',
'analyze faces',
'face detection',
'who is in this photo',
'faces in image',
'run face detection',
],
ย
inputSchema: {
input: {
type: 'string',
description: 'Local file path OR public image URL',
required: true,
}
},
ย
async run({ input } = {}) {
if (!input) return { success: false, error: 'No input provided. Pass a file path or image URL.' };
try {
const data = await detectFaces(input);
return {
success: true,
summary: \Detected \${data.faceCount} face(s).\
,
data,
};
} catch (err) {
return { success: false, error: err.message };
}
},
};
;
ย
const SKILL_TEST_JS =
/**
* Quick test for face-detection skill.
* Run: node test.js
*/
const skill = require('.');
const input = process.argv[2] || 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/14/Gatto_europeo4.jpg/320px-Gatto_europeo4.jpg';
ย
console.log('Testing face-detection skill...');
console.log('Input:', input);
ย
skill.run({ input }).then(result => {
console.log(JSON.stringify(result, null, 2));
}).catch(err => {
console.error('Test failed:', err.message);
process.exit(1);
});
;
ย
// โโโ MAIN INSTALLER โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ย
async function install() {
console.log('\nโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
console.log('โ Hermes Skill Installer: face-detection โ');
console.log(
โ Skills dir: ${SKILLS_DIR.slice(0, 32).padEnd(32)} โ);
console.log(
โ Dry run: ${DRY_RUN ? 'YES' : 'NO '} โ);
console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n');
ย
// โโ Step 1: System check โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
log.info('Step 1/6: Checking system requirements...');
try {
const nodeVer = process.version;
const [major] = nodeVer.replace('v','').split('.').map(Number);
if (major < 16) throw new Error(
Node.js 16+ required, found ${nodeVer});
log.ok(
Node.js ${nodeVer} โ);
} catch (e) { log.error(e.message); process.exit(1); }
ย
try {
sh('which python3 || which python');
log.ok('python3 found โ');
} catch { log.warn('python3 not found โ @tensorflow/tfjs-node may fail to compile'); }
ย
try {
sh('dpkg -l build-essential 2>/dev/null | grep -q "^ii" || gcc --version');
log.ok('build-essential found โ');
} catch {
log.warn('build-essential may not be installed. Running: sudo apt install -y build-essential python3');
try { sh('sudo apt install -y build-essential python3 2>&1'); log.ok('build-essential installed โ'); }
catch { log.warn('Could not auto-install build-essential. Manual install may be required.'); }
}
ย
// โโ Step 2: Directory setup โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
log.info('Step 2/6: Creating skill directories...');
for (const dir of [SKILLS_DIR, SKILL_DIR, MODELS_DIR]) {
if (!fs.existsSync(dir)) {
if (!DRY_RUN) fs.mkdirSync(dir, { recursive: true });
log.ok(
Created: ${dir});
} else {
log.skip(
Exists: ${dir});
}
}
ย
// โโ Step 3: npm packages โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
log.info('Step 3/6: Installing npm packages (this may take 2โ5 minutes)...');
const pkgJson = path.join(SKILL_DIR, 'package.json');
if (!fs.existsSync(pkgJson)) {
sh(
cd "${SKILL_DIR}" && npm init -y);
}
for (const pkg of NPM_PACKAGES) {
const pkgDir = path.join(SKILL_DIR, 'node_modules', pkg.replace('/', path.sep));
if (fs.existsSync(pkgDir)) {
log.skip(
Already installed: ${pkg});
} else {
log.info(
Installing: ${pkg});
sh(
cd "${SKILL_DIR}" && npm install ${pkg} --save 2>&1);
log.ok(
Installed: ${pkg});
}
}
ย
// โโ Step 4: Download models โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
log.info('Step 4/6: Downloading face detection models...');
for (const model of REQUIRED_MODELS) {
await downloadFile(
${MODEL_BASE_URL}/${model}, path.join(MODELS_DIR, model));
}
log.ok(
All ${REQUIRED_MODELS.length} model files ready โ);
ย
// โโ Step 5: Write skill files โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
log.info('Step 5/6: Writing skill source files...');
const indexPath = path.join(SKILL_DIR, 'index.js');
const testPath = path.join(SKILL_DIR, 'test.js');
ย
if (!DRY_RUN) {
fs.writeFileSync(indexPath, SKILL_INDEX_JS, 'utf8');
fs.writeFileSync(testPath, SKILL_TEST_JS, 'utf8');
}
log.ok(
Wrote: ${indexPath});
log.ok(
Wrote: ${testPath});
ย
// โโ Step 6: Register in skills registry โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
log.info('Step 6/6: Registering skill in skills-registry.json...');
updateRegistry({
name: 'face-detection',
version: '1.0.0',
description: 'Detect faces in local images or public URLs. Returns count, bounding boxes, confidence, and expressions.',
entryPoint: path.join(SKILL_DIR, 'index.js'),
triggers: ['detect faces', 'how many faces', 'analyze faces', 'face detection', 'faces in image'],
installedAt: new Date().toISOString(),
dependencies: NPM_PACKAGES,
});
ย
// โโ Done โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
console.log('\nโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
console.log('โ โ face-detection skill installed! โ');
console.log('โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ');
console.log(
โ Skill dir: ${SKILL_DIR.slice(-32).padEnd(32)} โ);
console.log(
โ Models: ${String(REQUIRED_MODELS.length).padEnd(32)} โ);
console.log('โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ');
console.log('โ Test it: โ');
console.log(
โ node "${path.join(SKILL_DIR, 'test.js')}".slice(0, 48).padEnd(48) + 'โ');
console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n');
}
ย
install().catch(err => {
log.error(
Install failed: ${err.message});
process.exit(1);
});