(function () {
"use strict";
// hermes-achievements dashboard plugin
// Originally authored by @PCinkusz — https://github.com/PCinkusz/hermes-achievements (MIT).
// Bundled into hermes-agent. Upstream repo remains the staging ground for new
// badges and UI iteration; the in-progress scan banner below is a small addition
// layered on top of the original dist bundle.
const SDK = window.__HERMES_PLUGIN_SDK__;
if (!SDK || !window.__HERMES_PLUGINS__) return;
const React = SDK.React;
const hooks = SDK.hooks;
const C = SDK.components;
const cn = SDK.utils.cn;
const LUCIDE = {"flame":"","avalanche":"\n ","nodes":"\n \n \n \n ","rocket":"\n \n \n ","branch":"\n \n \n ","daemon":"\n ","clock":"\n ","warning":"\n \n ","wine":"\n \n \n ","scroll":"\n \n \n ","plug":"\n \n \n \n \n ","lock":"\n \n ","package_skull":"\n \n \n \n ","restart":"\n \n \n ","key":"\n ","colon":"\n ","container":"\n \n \n \n ","melting_clock":"\n \n ","pencil":"\n ","blueprint":"\n \n \n \n ","pixel":"\n \n \n \n ","ship":"\n \n \n \n ","spark_cursor":"\n \n \n \n ","needle":"","hammer_scroll":"\n \n ","anvil":"\n \n \n \n ","crystal":"\n \n ","palace":"\n \n \n \n \n ","dragon":"","antenna":"\n \n \n \n \n \n ","puzzle":"","rewind":"\n ","spiral":"\n \n \n \n ","quote":"\n ","compass":"\n ","browser":"\n \n ","terminal":"\n ","wand":"\n \n \n \n \n \n \n ","folder":"\n \n ","eye":"\n ","wave":"","swap":"\n \n \n ","router":"\n \n \n \n \n ","codex":"\n \n ","prism":"\n \n ","marathon":"\n \n ","calendar":"\n \n \n \n \n \n \n \n \n ","moon":"","cache":"\n \n ","secret":"\n \n "};
const tierClass = function (tier) {
return tier ? "ha-tier-" + tier.toLowerCase() : "ha-tier-pending";
};
async function api(path, options) {
const url = "/api/plugins/hermes-achievements" + path;
const res = await fetch(url, options || {});
if (!res.ok) {
const text = await res.text().catch(function () { return res.statusText; });
throw new Error(res.status + ": " + text);
}
const text = await res.text();
try {
return JSON.parse(text);
} catch (_) {
return null;
}
}
function AchievementIcon({ icon }) {
const svg = LUCIDE[icon] || LUCIDE.secret;
const ref = React.useRef(null);
React.useEffect(function () {
if (!ref.current) return;
const el = ref.current;
while (el.firstChild) el.removeChild(el.firstChild);
try {
const doc = new DOMParser().parseFromString(
"",
"image/svg+xml"
);
if (!doc.querySelector("parsererror")) {
Array.from(doc.documentElement.childNodes).forEach(function (n) {
el.appendChild(document.importNode(n, true));
});
}
} catch (_) {}
}, [svg]);
return React.createElement("svg", {
ref: ref,
className: "ha-lucide",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 2,
strokeLinecap: "round",
strokeLinejoin: "round",
"aria-hidden": "true",
});
}
function StatCard(props) {
return React.createElement(C.Card, { className: "ha-stat" },
React.createElement(C.CardContent, { className: "ha-stat-content" },
React.createElement("div", { className: "ha-stat-label" }, props.label),
React.createElement("div", { className: "ha-stat-value" }, props.value),
props.hint && React.createElement("div", { className: "ha-stat-hint" }, props.hint)
)
);
}
function TierLegend() {
return React.createElement("div", { className: "ha-tier-legend" },
["Copper", "Silver", "Gold", "Diamond", "Olympian"].map(function (tier, index, arr) {
return React.createElement(React.Fragment, { key: tier },
React.createElement("span", { className: "ha-tier-step ha-tier-" + tier.toLowerCase() },
React.createElement("i", null),
tier
),
index < arr.length - 1 && React.createElement("span", { className: "ha-tier-arrow" }, "→")
);
})
);
}
function LoadingSkeletonCard(props) {
return React.createElement(C.Card, { className: "ha-card ha-skeleton-card ha-tier-pending" },
React.createElement(C.CardContent, { className: "ha-card-content" },
React.createElement("div", { className: "ha-card-head" },
React.createElement("div", { className: "ha-skeleton ha-skeleton-icon" }),
React.createElement("div", { className: "ha-skeleton-stack" },
React.createElement("div", { className: "ha-skeleton ha-skeleton-title" }),
React.createElement("div", { className: "ha-skeleton ha-skeleton-meta" })
),
React.createElement("div", { className: "ha-badges" },
React.createElement("div", { className: "ha-skeleton ha-skeleton-badge" }),
React.createElement("div", { className: "ha-skeleton ha-skeleton-badge ha-skeleton-badge-short" })
)
),
React.createElement("div", { className: "ha-skeleton ha-skeleton-line" }),
React.createElement("div", { className: "ha-skeleton ha-skeleton-line ha-skeleton-line-short" }),
React.createElement("div", { className: "ha-skeleton ha-skeleton-criteria" }),
React.createElement("div", { className: "ha-evidence-slot" }, React.createElement("div", { className: "ha-skeleton ha-skeleton-evidence" })),
React.createElement("div", { className: "ha-progress-row" },
React.createElement("div", { className: "ha-skeleton ha-skeleton-progress" }),
React.createElement("div", { className: "ha-skeleton ha-skeleton-progress-text" })
)
)
);
}
function LoadingPage() {
return React.createElement("div", { className: "ha-page ha-page-loading" },
React.createElement("section", { className: "ha-hero ha-loading-hero" },
React.createElement("div", null,
React.createElement("div", { className: "ha-kicker" }, "Agentic Gamerscore"),
React.createElement("h1", null, "Hermes Achievements"),
React.createElement("p", null, "Scanning Hermes session history. First scan can take 5–10 seconds on large histories.")
),
React.createElement("div", { className: "ha-scan-status", role: "status", "aria-live": "polite" },
React.createElement("span", { className: "ha-scan-pulse", "aria-hidden": "true" }),
React.createElement("div", null,
React.createElement("strong", null, "Building achievement profile…"),
React.createElement("p", null, "Reading sessions, tool calls, model metadata, and unlock state.")
)
)
),
React.createElement("div", { className: "ha-stats" },
["Unlocked", "Discovered", "Secrets", "Highest tier", "Latest"].map(function (label) {
return React.createElement(C.Card, { key: label, className: "ha-stat ha-skeleton-stat" },
React.createElement(C.CardContent, { className: "ha-stat-content" },
React.createElement("div", { className: "ha-stat-label" }, label),
React.createElement("div", { className: "ha-skeleton ha-skeleton-stat-value" }),
React.createElement("div", { className: "ha-skeleton ha-skeleton-stat-hint" })
)
);
})
),
React.createElement("section", { className: "ha-guide ha-loading-guide" },
React.createElement("div", null,
React.createElement("strong", null, "Scan status"),
React.createElement("p", null, "Hermes is scanning local history once, then cards will appear automatically. Nothing is stuck if this takes a few seconds.")
),
React.createElement("div", null,
React.createElement("strong", null, "What is scanned"),
React.createElement("p", null, "Sessions, tool calls, model metadata, errors, achievements, and local unlock state.")
)
),
React.createElement("section", { className: "ha-grid" }, [0, 1, 2, 3, 4, 5].map(function (i) {
return React.createElement(LoadingSkeletonCard, { key: i });
}))
);
}
function AchievementCard({ achievement }) {
const unlocked = achievement.unlocked;
const progress = achievement.progress || 0;
const pct = achievement.progress_pct || (unlocked ? 100 : 0);
const state = achievement.state || (unlocked ? "unlocked" : "discovered");
const stateLabel = state === "unlocked" ? "Unlocked" : (state === "secret" ? "Secret" : "Discovered");
const targetTier = achievement.next_tier || achievement.tier;
const tierLabel = achievement.tier ? achievement.tier : (targetTier ? "Target " + targetTier : (state === "secret" ? "Hidden" : (unlocked ? "Complete" : "Objective")));
const progressText = state === "secret" ? "hidden" : (progress + (achievement.next_threshold ? " / " + achievement.next_threshold : ""));
return React.createElement(C.Card, { className: cn("ha-card", "ha-state-" + state, tierClass(achievement.tier || achievement.next_tier)) },
React.createElement(C.CardContent, { className: "ha-card-content" },
React.createElement("div", { className: "ha-card-head" },
React.createElement("div", { className: "ha-icon" }, React.createElement(AchievementIcon, { icon: achievement.icon || "secret" })),
React.createElement("div", { className: "ha-card-title-wrap" },
React.createElement("div", { className: "ha-card-title" }, achievement.name),
React.createElement("div", { className: "ha-card-category" }, achievement.category)
),
React.createElement("div", { className: "ha-badges" },
React.createElement("span", { className: "ha-state-badge" }, stateLabel),
React.createElement("span", { className: "ha-tier-badge" }, tierLabel)
)
),
React.createElement("p", { className: "ha-description" }, achievement.description),
achievement.criteria && React.createElement("details", { className: "ha-criteria" },
React.createElement("summary", null, state === "secret" ? "How to reveal" : "What counts"),
React.createElement("p", null, achievement.criteria)
),
React.createElement("div", { className: "ha-evidence-slot" },
achievement.evidence ? React.createElement("div", { className: "ha-evidence" },
React.createElement("span", { className: "ha-evidence-label" }, "Evidence"),
React.createElement("span", { className: "ha-evidence-title" }, achievement.evidence.title || achievement.evidence.session_id || "session")
) : React.createElement("div", { className: "ha-evidence ha-evidence-empty", "aria-hidden": "true" }, "No evidence yet")
),
React.createElement("div", { className: "ha-progress-row" },
React.createElement("div", { className: "ha-progress-track" },
React.createElement("div", { className: "ha-progress-fill", style: { width: Math.max(state === "secret" ? 0 : 3, Math.min(100, pct)) + "%" } })
),
React.createElement("span", { className: "ha-progress-text" }, progressText)
)
)
);
}
function AchievementsPage() {
const [data, setData] = hooks.useState(null);
const [loading, setLoading] = hooks.useState(true);
const [error, setError] = hooks.useState(null);
const [category, setCategory] = hooks.useState("All");
const [visibility, setVisibility] = hooks.useState("all");
function load() {
setLoading(true);
api("/achievements")
.then(function (payload) { setData(payload); setError((payload && payload.error) || null); })
.catch(function (err) { setError(String(err)); })
.finally(function () { setLoading(false); });
}
// refresh() re-fetches without flipping the loading state — used by the
// auto-poller during an in-progress background scan so the page updates
// with growing unlock counts instead of flashing the loading skeleton.
function refresh() {
api("/achievements")
.then(function (payload) { setData(payload); setError((payload && payload.error) || null); })
.catch(function (err) { setError(String(err)); });
}
hooks.useEffect(load, []);
// Auto-poll while the backend is still scanning. scan_meta.mode is
// "pending" on the very first request (no cache yet) and "in_progress"
// while the background thread is publishing partial snapshots. Once it
// flips to "full" or "incremental" the scan is done and we stop polling.
const scanMode = (data && data.scan_meta && data.scan_meta.mode) || null;
const scanInFlight = scanMode === "pending" || scanMode === "in_progress";
hooks.useEffect(function () {
if (!scanInFlight) return undefined;
const id = setInterval(refresh, 4000);
return function () { clearInterval(id); };
}, [scanInFlight]);
const achievements = (data && data.achievements) || [];
const categories = ["All"].concat(Array.from(new Set(achievements.map(function (a) { return a.category; }))));
const visible = achievements.filter(function (a) {
if (category !== "All" && a.category !== category) return false;
if (visibility === "unlocked" && a.state !== "unlocked") return false;
if (visibility === "discovered" && a.state !== "discovered") return false;
if (visibility === "secret" && a.state !== "secret") return false;
return true;
});
const unlocked = achievements.filter(function (a) { return a.state === "unlocked"; });
const discovered = achievements.filter(function (a) { return a.state === "discovered"; });
const secret = achievements.filter(function (a) { return a.state === "secret"; });
const latest = unlocked.slice().sort(function (a, b) { return (b.unlocked_at || 0) - (a.unlocked_at || 0); }).slice(0, 5);
const highest = ["Olympian", "Diamond", "Gold", "Silver", "Copper"].find(function (tier) { return unlocked.some(function (a) { return a.tier === tier; }); }) || "None yet";
// Build the in-progress scan banner once so the JSX below stays readable.
// Shows nothing when the scan is idle. When a scan is running it renders
// a pulsing status row with "X / Y sessions · Z%" and a filling bar, so
// the user gets continuous visual feedback during long cold scans on
// large session databases (can take several minutes on 8000+ sessions).
let scanBanner = null;
if (scanInFlight) {
const meta = (data && data.scan_meta) || {};
const scanned = Number(meta.sessions_scanned_so_far || meta.sessions_total || 0);
const total = Number(meta.sessions_expected_total || 0);
const pct = total > 0 ? Math.max(0, Math.min(100, Math.floor((scanned / total) * 100))) : 0;
const headline = scanMode === "pending"
? "Starting achievement scan…"
: "Building achievement profile…";
const detail = total > 0
? ("Scanned " + scanned.toLocaleString() + " of " + total.toLocaleString() + " sessions · " + pct + "%. Badges unlock as more history streams in.")
: "Reading sessions, tool calls, model metadata, and unlock state. Badges appear here as they unlock.";
scanBanner = React.createElement("section", { className: "ha-scan-banner", role: "status", "aria-live": "polite" },
React.createElement("div", { className: "ha-scan-banner-head" },
React.createElement("span", { className: "ha-scan-pulse", "aria-hidden": "true" }),
React.createElement("div", { className: "ha-scan-banner-text" },
React.createElement("strong", null, headline),
React.createElement("p", null, detail)
)
),
total > 0 && React.createElement("div", { className: "ha-scan-progress-track", role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": pct },
React.createElement("div", { className: "ha-scan-progress-fill", style: { width: pct + "%" } })
)
);
}
if (loading) {
return React.createElement(LoadingPage, null);
}
return React.createElement("div", { className: "ha-page" },
React.createElement("section", { className: "ha-hero" },
React.createElement("div", null,
React.createElement("div", { className: "ha-kicker" }, "Agentic Gamerscore"),
React.createElement("h1", null, "Hermes Achievements"),
React.createElement("p", null, "Collectible Hermes badges earned from real session history. Known unfinished achievements are shown as Discovered; Secret achievements stay hidden until the first matching behavior appears.")
),
React.createElement(C.Button, { onClick: load, className: "ha-refresh" }, "Rescan")
),
scanBanner,
error && React.createElement(C.Card, { className: "ha-error" }, React.createElement(C.CardContent, null, String(error))),
React.createElement("div", { className: "ha-stats" },
React.createElement(StatCard, { label: "Unlocked", value: (data ? data.unlocked_count : 0) + " / " + (data ? data.total_count : 0), hint: "earned badges" }),
React.createElement(StatCard, { label: "Discovered", value: discovered.length, hint: "known, not earned yet" }),
React.createElement(StatCard, { label: "Secrets", value: secret.length, hint: "hidden until first signal" }),
React.createElement(StatCard, { label: "Highest tier", value: highest, hint: "Copper → Silver → Gold → Diamond → Olympian" }),
React.createElement(StatCard, { label: "Latest", value: latest[0] ? latest[0].name : "None yet", hint: latest[0] ? latest[0].category : "run Hermes more" })
),
React.createElement("section", { className: "ha-guide" },
React.createElement("div", null,
React.createElement("strong", null, "Tiers"),
React.createElement(TierLegend, null)
),
React.createElement("div", null,
React.createElement("strong", null, "Secret achievements"),
React.createElement("p", null, "Secrets hide their exact trigger. Once Hermes sees a related signal, the card becomes Discovered and shows its requirement.")
)
),
React.createElement("div", { className: "ha-toolbar" },
React.createElement("div", { className: "ha-pills" }, categories.map(function (cat) {
return React.createElement("button", { key: cat, onClick: function () { setCategory(cat); }, className: cat === category ? "active" : "" }, cat);
})),
React.createElement("div", { className: "ha-pills" }, ["all", "unlocked", "discovered", "secret"].map(function (v) {
return React.createElement("button", { key: v, onClick: function () { setVisibility(v); }, className: v === visibility ? "active" : "" }, v);
}))
),
latest.length > 0 && React.createElement("section", { className: "ha-latest" },
React.createElement("h2", null, "Recent unlocks"),
React.createElement("div", { className: "ha-latest-row" }, latest.map(function (a) {
return React.createElement("div", { key: a.id, className: cn("ha-chip", tierClass(a.tier)) },
React.createElement("span", { className: "ha-chip-icon" }, React.createElement(AchievementIcon, { icon: a.icon || "secret" })),
a.name
);
}))
),
visibility === "secret" && visible.length === 0 && React.createElement(C.Card, { className: "ha-secret-empty" },
React.createElement(C.CardContent, { className: "ha-secret-empty-content" },
React.createElement("strong", null, "No hidden secrets left in this scan."),
React.createElement("p", null, "Clue: secrets usually start from unusual failure or power-user patterns — port conflicts, permission walls, missing env vars, YAML mistakes, Docker collisions, rollback/checkpoint use, cache hits, or tiny fixes after lots of red text.")
)
),
React.createElement("section", { className: "ha-grid" }, visible.map(function (a) {
return React.createElement(AchievementCard, { key: a.id, achievement: a });
}))
);
}
window.__HERMES_PLUGINS__.register("hermes-achievements", AchievementsPage);
})();