import { defineCommand } from "citty"; import { trackEvent, flush } from "../telemetry/client.js"; import { SKILL_SLUG } from "../telemetry/skill.js"; // Skill-usage telemetry endpoint. A skill reports its own invocation/outcome — // ideally from its own bundled script, so it fires deterministically rather // than relying on the agent to remember: // // npx hyperframes events --skill=product-launch-video // npx hyperframes events --skill=product-launch-video --event=skill_completed --outcome=success // // Rides the SAME anonymous PostHog pipeline + consent gates as every other CLI // event (DO_NOT_TRACK / telemetry opt-out, anonymous install UUID, IP stripped). // // Telemetry must NEVER break the calling skill: every arg is optional (a missing // or malformed value is a silent no-op, not a non-zero exit), the body is // guarded, and flush() carries its own hard timeout. This command always exits 0. const ALLOWED_EVENTS = ["skill_invoked", "skill_completed"]; const ALLOWED_OUTCOMES = ["success", "error", "abort"]; export default defineCommand({ meta: { name: "events", description: "Emit an anonymous skill-usage telemetry event (skills report their own invocation/outcome). Honors DO_NOT_TRACK / telemetry opt-out.", }, args: { skill: { type: "string", description: "Authoring skill slug, e.g. product-launch-video", }, event: { type: "string", description: "Event name: skill_invoked | skill_completed (default: skill_invoked)", default: "skill_invoked", }, outcome: { type: "string", description: "Optional outcome for completion events: success | error | abort", }, }, async run({ args }) { // Best-effort: nothing here may fail the skill that called us. Missing or // malformed input is a silent no-op rather than a non-zero exit. try { const skill = typeof args.skill === "string" ? args.skill.trim() : ""; if (!SKILL_SLUG.test(skill)) return; // missing / non-slug → no-op const event = ALLOWED_EVENTS.includes(args.event) ? args.event : "skill_invoked"; const props: Record = { authoring_skill: skill }; if (args.outcome && ALLOWED_OUTCOMES.includes(args.outcome)) { props["outcome"] = args.outcome; } trackEvent(event, props); await flush(); } catch { // swallow — telemetry must never surface a non-zero exit to the caller } }, });