#!/usr/bin/env bun /** * compliance-report-pack * Assembles compliance reports with evidence and remediation steps using OpenAI. */ import { parseArgs } from "util"; import { existsSync, mkdirSync, appendFileSync, readFileSync } from "fs"; import { join, dirname, resolve } from "path"; import { randomUUID } from "crypto"; type OutputFormat = "markdown" | "json"; type ComplianceTemplate = { controls?: Array>; findings?: Array>; evidence?: Array>; notes?: string; }; interface SkillOptions { dataset: string; framework?: string; period?: string; audience?: string; evidenceRoot?: string; format: OutputFormat; model: string; output?: string; template?: ComplianceTemplate; } interface OpenAIChatResponse { choices?: Array<{ message?: { content?: string | null; }; }>; error?: { message?: string; }; } const SKILL_SLUG = "compliance-report-pack"; const SESSION_ID = randomUUID().slice(0, 8); function ensureDir(path: string) { if (!existsSync(path)) { mkdirSync(path, { recursive: true }); } } function getPaths() { const sessionStamp = new Date().toISOString().replace(/[:.]/g, "_").replace(/-/g, "_"); const exportsRoot = process.env.SKILLS_EXPORTS_DIR || join(process.cwd(), ".skills", "exports"); const logsRoot = process.env.SKILLS_LOGS_DIR || join(process.cwd(), ".skills", "logs"); const skillExportsDir = join(exportsRoot, SKILL_SLUG); const skillLogsDir = join(logsRoot, SKILL_SLUG); ensureDir(skillExportsDir); ensureDir(skillLogsDir); return { sessionStamp, skillExportsDir, skillLogsDir, }; } function createLogger(logDir: string, sessionStamp: string) { const logFile = join(logDir, `log_${sessionStamp}_${SESSION_ID}.log`); function write(level: "info" | "success" | "error", message: string) { const timestamp = new Date().toISOString(); const entry = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`; appendFileSync(logFile, entry); const prefix = level === "success" ? "✅" : level === "error" ? "❌" : "â„šī¸"; if (level === "error") { console.error(`${prefix} ${message}`); } else { console.log(`${prefix} ${message}`); } } return { info: (message: string) => write("info", message), success: (message: string) => write("success", message), error: (message: string) => write("error", message), logFile, }; } function slugify(value: string): string { return value .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 40) || "compliance-pack"; } function parseJsonTemplate(content: string): ComplianceTemplate | undefined { try { const data = JSON.parse(content); if (typeof data === "object" && data !== null) { return data as ComplianceTemplate; } } catch (_error) { // treat as plain text when parsing fails } return undefined; } function parseOptions(): SkillOptions { const { values, positionals } = parseArgs({ args: Bun.argv.slice(2), options: { text: { type: "string" }, framework: { type: "string" }, period: { type: "string" }, audience: { type: "string" }, "evidence-root": { type: "string" }, format: { type: "string", default: "markdown" }, model: { type: "string", default: "gpt-4o-mini" }, output: { type: "string" }, help: { type: "boolean", short: "h" }, }, allowPositionals: true, }); if (values.help) { console.log(` Compliance Report Pack - Assembles compliance reports Usage: skills run compliance-report-pack -- [options] Options: --text Compliance data (or use positional arg) --framework Compliance framework (e.g. SOC2) --period Reporting period --audience Target audience --evidence-root Path to evidence files --format Output format: markdown, json (default: markdown) --model OpenAI model to use (default: gpt-4o-mini) --output Save report to file --help, -h Show this help `); process.exit(0); } let dataset = values.text || ""; let template: ComplianceTemplate | undefined; if (!dataset && positionals[0]) { const filePath = resolve(positionals[0]); if (existsSync(filePath)) { const content = readFileSync(filePath, "utf-8"); template = parseJsonTemplate(content); dataset = template ? "" : content; } else { dataset = positionals.join(" "); } } if (!dataset.trim() && !template) { throw new Error("Provide compliance data via positional text, file path, or --text."); } const format: OutputFormat = values.format === "json" ? "json" : values.format === "markdown" ? "markdown" : "markdown"; return { dataset, framework: values.framework as string, period: values.period as string, audience: values.audience as string, evidenceRoot: values["evidence-root"] as string, format, model: values.model as string, output: values.output as string, template, }; } function buildPrompt(options: SkillOptions) { const system = `You are a compliance manager compiling an audit-ready report. Summarize control posture, cite evidence, highlight gaps, and outline remediation plans. Tailor tone to the specified audience and framework.`; const instructions = options.format === "json" ? "Respond in JSON with keys: summary, control_status, evidence, findings, remediation, timeline. Each finding should include control, severity, owner, due_date, status." : "Respond in polished Markdown. Start with an executive summary, list control status table (control, status, evidence, owner), document findings with severity, outline remediation actions with timelines, and include evidence appendix references."; const payload = { framework: options.framework || "SOC2", period: options.period || "current quarter", audience: options.audience || "CISO", evidence_root: options.evidenceRoot || "evidence/", dataset_text: options.dataset.substring(0, 6000), structured_template: options.template, }; const user = `${instructions}\n\n${JSON.stringify(payload, null, 2)}`; return { system, user }; } async function callOpenAI(options: SkillOptions, system: string, user: string): Promise { const apiKey = process.env.OPENAI_API_KEY; if (!apiKey) { throw new Error("OPENAI_API_KEY environment variable is required."); } const body = { model: options.model, messages: [ { role: "system", content: system }, { role: "user", content: user }, ], temperature: 0.31, max_tokens: options.format === "json" ? 2200 : 2000, }; const response = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify(body), }); const data: OpenAIChatResponse = await response.json(); if (!response.ok) { throw new Error(data.error?.message || `OpenAI API error (${response.status})`); } const content = data.choices?.[0]?.message?.content; if (!content) { throw new Error("OpenAI response did not include content."); } return content.trim(); } async function writeExport(path: string, content: string) { ensureDir(dirname(path)); await Bun.write(path, content); } function buildExportPath(skillExportsDir: string, sessionStamp: string, options: SkillOptions) { if (options.output) { return resolve(options.output); } const descriptorParts = [options.framework || "framework", options.period || "period"]; const base = slugify(descriptorParts.filter(Boolean).join("-")); const extension = options.format === "json" ? "json" : "md"; return join(skillExportsDir, `${base}_${sessionStamp}.${extension}`); } function preview(content: string) { const lines = content.split(/\r?\n/).slice(0, 8); lines.forEach(line => console.log(` ${line}`)); if (content.split(/\r?\n/).length > 8) { console.log(" ..."); } } async function main() { const { sessionStamp, skillExportsDir, skillLogsDir } = getPaths(); const logger = createLogger(skillLogsDir, sessionStamp); try { logger.info(`Starting ${SKILL_SLUG} session: ${SESSION_ID}`); const options = parseOptions(); logger.info("Parsed compliance inputs and options."); const { system, user } = buildPrompt(options); logger.info("Constructed compliance report prompt."); const content = await callOpenAI(options, system, user); logger.success("Received compliance report pack from OpenAI."); const exportPath = buildExportPath(skillExportsDir, sessionStamp, options); await writeExport(exportPath, content); logger.success(`Saved compliance pack to ${exportPath}`); console.log("\nPreview:"); preview(content); } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.error(message); process.exitCode = 1; } } main();