#!/usr/bin/env bun /** * contract-plainlanguage * Summarizes contract clauses in plain language 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 PlainTemplate = { clauses?: Array>; notes?: string; }; interface SkillOptions { clauses: string; audience?: string; jurisdiction?: string; riskLevel?: string; format: OutputFormat; model: string; output?: string; template?: PlainTemplate; } interface OpenAIChatResponse { choices?: Array<{ message?: { content?: string | null; }; }>; error?: { message?: string; }; } const SKILL_SLUG = "contract-plainlanguage"; 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) || "plainlanguage"; } function parseJsonTemplate(content: string): PlainTemplate | undefined { try { const data = JSON.parse(content); if (typeof data === "object" && data !== null) { return data as PlainTemplate; } } 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" }, audience: { type: "string" }, jurisdiction: { type: "string" }, "risk-level": { 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(` Contract Plain Language - Summarizes contract clauses Usage: skills run contract-plainlanguage -- [options] Options: --text Contract clauses (or use positional arg) --audience Target audience --jurisdiction Jurisdiction --risk-level Risk level threshold --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 clauses = values.text || ""; let template: PlainTemplate | undefined; if (!clauses && positionals[0]) { const filePath = resolve(positionals[0]); if (existsSync(filePath)) { const content = readFileSync(filePath, "utf-8"); template = parseJsonTemplate(content); clauses = template ? "" : content; } else { clauses = positionals.join(" "); } } if (!clauses.trim() && !template) { throw new Error("Provide contract clauses via positional text, file path, or --text."); } const format: OutputFormat = values.format === "json" ? "json" : values.format === "markdown" ? "markdown" : "markdown"; return { clauses, audience: values.audience as string, jurisdiction: values.jurisdiction as string, riskLevel: values["risk-level"] as string, format, model: values.model as string, output: values.output as string, template, }; } function buildPrompt(options: SkillOptions) { const system = `You are a technology transactions lawyer translating contract clauses into plain language. Explain obligations, risks, and action items plainly while preserving legal accuracy. Flag clauses at or above the specified risk level and suggest follow-ups.`; const instructions = options.format === "json" ? "Respond in JSON with keys: summary, clauses, risks, obligations, actions. Each clause entry should include title, plain_language, risk_level, obligations, counterparty_actions." : "Respond in polished Markdown. Start with an executive summary, provide clause-by-clause headings with plain-language summaries, list obligations and counterpart responsibilities, flag risks, and recommend follow-up actions."; const payload = { audience: options.audience || "Legal", jurisdiction: options.jurisdiction || "General", risk_level: options.riskLevel || "medium", clauses_text: options.clauses.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.28, 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.audience || "audience", options.riskLevel || "risk"]; 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 contract clauses and options."); const { system, user } = buildPrompt(options); logger.info("Constructed plain-language prompt."); const content = await callOpenAI(options, system, user); logger.success("Received plain-language summary from OpenAI."); const exportPath = buildExportPath(skillExportsDir, sessionStamp, options); await writeExport(exportPath, content); logger.success(`Saved summary 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();