import { parse as parseJsonc } from "jsonc-parser"; import type { PreviewItem } from "../../components/share-home-types.ts"; import { humanizeType, maybeArray, maybeString, parseFrontmatter } from "./share-utils.ts"; import type { Frontmatter, NormalizedFile, PackageInput, PackageResult, PackageSummary } from "./types.ts"; const MAX_FILES = 200; const SECRET_KEY_RE = /(token|secret|password|api[-_]?key|authorization|bearer|private[-_]?key|client[-_]?secret)/i; const SAFE_SECRET_VALUE_RE = /^(\$\{|\{env:|env\.|process\.env\.|<|YOUR_|REPLACE_ME|example|changeme)/i; const AGENT_FRONTMATTER_KEYS = new Set(["mode", "model", "tools", "permission", "temperature", "color", "prompt"]); const OPENCODE_CONFIG_KEYS = new Set(["model", "autoupdate", "server", "provider", "plugin", "mcp", "agent", "permission"]); function normalizePath(input: unknown): string { return String(input ?? "") .replaceAll("\\", "/") .replace(/\/+/g, "/") .replace(/^\.\//, "") .trim(); } function basename(path: string): string { const normalized = normalizePath(path); if (!normalized) return ""; return normalized.split("/").pop() ?? normalized; } function dirname(path: string): string { const normalized = normalizePath(path); if (!normalized || !normalized.includes("/")) return ""; return normalized.slice(0, normalized.lastIndexOf("/")); } function stem(path: string): string { const name = basename(path); return name.replace(/\.[^.]+$/, ""); } function slugify(value: unknown): string { return String(value ?? "") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 64); } function normalizeFile(input: { path?: string; webkitRelativePath?: string; name?: string; content?: string }, index: number): NormalizedFile { const path = normalizePath(input.path || input.webkitRelativePath || input.name || `file-${index + 1}`); const name = basename(path) || `file-${index + 1}`; const content = String(input.content ?? "").replace(/^\uFEFF/, ""); return { path, name, content }; } function isMarkdownFile(file: NormalizedFile): boolean { return /\.md$/i.test(file.name); } function isJsonFile(file: NormalizedFile): boolean { return /\.jsonc?$/i.test(file.name); } function isSkillFile(file: NormalizedFile): boolean { const lowerPath = file.path.toLowerCase(); return /^(skills?|skill)\.md$/i.test(basename(lowerPath)) || /(^|\/)\.opencode\/skills\//.test(lowerPath) || /(^|\/)skills\//.test(lowerPath); } function isCommandFile(file: NormalizedFile): boolean { return /(^|\/)\.opencode\/commands\//.test(file.path.toLowerCase()) || /(^|\/)commands\//.test(file.path.toLowerCase()); } function isAgentFile(file: NormalizedFile, frontmatterData: Record): boolean { const lowerPath = file.path.toLowerCase(); if (/(^|\/)\.opencode\/agents\//.test(lowerPath) || /(^|\/)agents\//.test(lowerPath) || /(^|\/)agents?\.md$/i.test(lowerPath)) { return true; } return Object.keys(frontmatterData).some((key) => AGENT_FRONTMATTER_KEYS.has(key)); } function isOpenworkConfigFile(file: NormalizedFile): boolean { return /(^|\/)(?:\.opencode\/)?openwork\.jsonc?$/i.test(file.path); } function isOpencodeConfigFile(file: NormalizedFile): boolean { return /(^|\/)opencode\.jsonc?$/i.test(file.path); } function looksLikeNamedMcpFile(file: NormalizedFile): boolean { return /(^|\/)mcp\//i.test(file.path) || /\.mcp\.jsonc?$/i.test(file.name) || /(^|[._-])mcp(?:[_-]?config)?\.jsonc?$/i.test(file.name); } function looksLikeNamedAgentJsonFile(file: NormalizedFile): boolean { return /(^|\/)agents\//i.test(file.path) || /\.agent\.jsonc?$/i.test(file.name); } function resolveName(preferred: unknown, fallback: unknown): string { const fromPreferred = slugify(preferred); if (fromPreferred) return fromPreferred; const fromFallback = slugify(fallback); if (fromFallback) return fromFallback; return `item-${Date.now()}`; } function buildPreviewItem(name: string, kind: PreviewItem["kind"], meta: string, tone: PreviewItem["tone"]): PreviewItem { return { name, kind, meta, tone }; } function countMatches(text: string, token: string): number { if (!text) return 0; return (text.match(new RegExp(`\\b${token}\\b`, "gi")) || []).length; } function collectHeadings(content: string): string { return Array.from(content.matchAll(/^#{1,3}\s+(.+)$/gm)) .map((match) => match[1]) .join("\n"); } function inferMarkdownKind(file: NormalizedFile, frontmatterData: Record): "agent" | "skill" { const lowerPath = file.path.toLowerCase(); const lowerContent = file.content.toLowerCase(); const headings = collectHeadings(file.content).toLowerCase(); let agentScore = 0; let skillScore = 0; if (/^agents?\.md$/i.test(basename(lowerPath))) { agentScore += 4; } if (/^skills?\.md$/i.test(basename(lowerPath))) { skillScore += 4; } agentScore += countMatches(lowerContent, "agent"); skillScore += countMatches(lowerContent, "skill"); agentScore += countMatches(headings, "agent") * 2; skillScore += countMatches(headings, "skill") * 2; if ("trigger" in frontmatterData) { skillScore += 3; } return agentScore > skillScore ? "agent" : "skill"; } interface SkillRecord { name: string; description: string; trigger: string; content: string; preview: PreviewItem; } function buildSkillRecord(file: NormalizedFile, warnings: string[], frontmatter: Frontmatter): SkillRecord | null { const { data } = frontmatter; const parentName = basename(dirname(file.path)); const name = resolveName(data.name, parentName || stem(file.path)); const description = typeof data.description === "string" ? data.description.trim() : ""; const trigger = typeof data.trigger === "string" ? data.trigger.trim() : ""; const version = typeof data.version === "string" ? data.version.trim() : ""; if (!file.content.trim()) { warnings.push(`Ignored empty skill file: ${file.path}`); return null; } return { name, description, trigger, content: file.content, preview: buildPreviewItem(name, "Skill", version ? `v${version}` : trigger ? `Trigger · ${trigger}` : "Skill", "skill"), }; } interface AgentRecord { name: string; config: Record; preview: PreviewItem; } function buildAgentRecord(file: NormalizedFile, warnings: string[], frontmatter: Frontmatter): AgentRecord | null { const { data, body } = frontmatter; const name = resolveName(data.name, stem(file.path)); if (!name) { warnings.push(`Ignored agent without a valid name: ${file.path}`); return null; } const config: Record = { ...data }; delete config.name; const promptBody = body.trim(); if (promptBody && typeof config.prompt !== "string") { config.prompt = promptBody; } const version = typeof config.version === "string" ? config.version.trim() : ""; const model = typeof config.model === "string" ? config.model.trim() : ""; return { name, config, preview: buildPreviewItem(name, "Agent", version ? `v${version}` : model || "Agent config", "agent"), }; } interface CommandRecord { name: string; description: string; template: string; agent: string; model: string; subtask: boolean; preview: PreviewItem; } function buildCommandRecord(file: NormalizedFile, warnings: string[], frontmatter: Frontmatter): CommandRecord | null { const { data, body } = frontmatter; const name = resolveName(data.name, stem(file.path)); const template = body.trim(); if (!name || !template) { warnings.push(`Ignored command without template: ${file.path}`); return null; } return { name, description: typeof data.description === "string" ? data.description.trim() : "", template, agent: typeof data.agent === "string" ? data.agent.trim() : "", model: typeof data.model === "string" ? data.model.trim() : "", subtask: data.subtask === true, preview: buildPreviewItem(name, "Command", data.agent ? `Agent · ${data.agent}` : "Command", "command"), }; } function parseJsonConfig(file: NormalizedFile): unknown { try { return parseJsonc(file.content); } catch { throw new Error(`Could not parse ${file.path} as JSON/JSONC`); } } function isRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } function cloneRecord(value: T): T { return JSON.parse(JSON.stringify(value)); } function mergeConfigSections(target: Record | null, source: Record | null): Record { if (!source) return target ?? {}; const output: Record = isRecord(target) ? cloneRecord(target) : {}; for (const [key, value] of Object.entries(source)) { if (isRecord(value) && isRecord(output[key])) { output[key] = mergeConfigSections(output[key] as Record, value); continue; } output[key] = cloneRecord(value); } return output; } function buildConfigPreview(file: NormalizedFile, meta: string): PreviewItem { return buildPreviewItem(basename(file.path) || stem(file.path), "Config", meta, "config"); } function collectOpencodePreviewItems(parsed: Record): PreviewItem[] { const preview: PreviewItem[] = []; if (isRecord(parsed?.mcp) && Object.keys(parsed.mcp as Record).length) { for (const [name, entry] of Object.entries(parsed.mcp as Record)) { const meta = isRecord(entry) && typeof entry.type === "string" ? `${humanizeType(entry.type)} MCP` : "MCP config"; preview.push(buildPreviewItem(name, "MCP", meta, "mcp")); } } if (isRecord(parsed?.agent) && Object.keys(parsed.agent as Record).length) { for (const [name, entry] of Object.entries(parsed.agent as Record)) { const meta = isRecord(entry) && typeof entry.model === "string" ? entry.model : "Agent config"; preview.push(buildPreviewItem(name, "Agent", meta, "agent")); } } return preview; } function looksLikeMcpShape(parsed: unknown): boolean { if (!isRecord(parsed)) return false; const schema = maybeString(parsed.$schema).toLowerCase(); if (schema.includes("modelcontextprotocol") || schema.includes("mcp")) { return true; } if (maybeArray(parsed.packages).length || maybeArray(parsed.remotes).length) { return true; } const hasTransport = typeof parsed.type === "string" && (typeof parsed.url === "string" || typeof parsed.command === "string"); const hasCommandRuntime = typeof parsed.command === "string" || maybeArray(parsed.args).length > 0; return hasTransport || hasCommandRuntime; } function looksLikeOpencodeShape(parsed: unknown): boolean { if (!isRecord(parsed)) return false; const schema = maybeString(parsed.$schema).toLowerCase(); if (schema.includes("opencode") && schema.includes("config")) { return true; } let matchedKeys = 0; for (const key of Object.keys(parsed)) { if (OPENCODE_CONFIG_KEYS.has(key)) matchedKeys += 1; } return matchedKeys >= 2; } function looksLikeOpenworkShape(parsed: unknown): boolean { if (!isRecord(parsed)) return false; const schema = maybeString(parsed.$schema).toLowerCase(); return schema.includes("openwork") && schema.includes("config"); } function redactSecrets(value: unknown, prefix: string[] = [], hits: string[] = []): string[] { if (!value || typeof value !== "object") return hits; for (const [key, child] of Object.entries(value as Record)) { const nextPrefix = [...prefix, key]; if (SECRET_KEY_RE.test(key) && typeof child === "string" && child.trim() && !SAFE_SECRET_VALUE_RE.test(child.trim())) { hits.push(nextPrefix.join(".")); (value as Record)[key] = "********"; } if (typeof child === "object") { redactSecrets(child, nextPrefix, hits); } } return hits; } interface ConfigSection { opencode: Record | null; openwork: Record | null; config: Record; preview: PreviewItem[]; configCount: number; } function readConfigSection(file: NormalizedFile, warnings: string[]): ConfigSection | null { const parsed = parseJsonConfig(file); if (!isRecord(parsed)) { throw new Error(`Expected ${file.path} to contain an object`); } let opencode: Record | null = null; let openwork: Record | null = null; const config: Record = {}; const preview: PreviewItem[] = []; let configCount = 0; if (isOpenworkConfigFile(file)) { openwork = cloneRecord(parsed); preview.push(buildConfigPreview(file, "OpenWork config")); configCount += 1; } if (isOpencodeConfigFile(file)) { opencode = cloneRecord(parsed); preview.push(buildConfigPreview(file, "OpenCode config")); preview.push(...collectOpencodePreviewItems(parsed)); configCount += 1; } if (!opencode && !openwork) { if (looksLikeNamedMcpFile(file)) { const name = resolveName("", stem(file.path)); opencode = { mcp: { [name]: cloneRecord(parsed) } }; const meta = typeof parsed.type === "string" ? `${humanizeType(parsed.type)} MCP` : "MCP config"; preview.push(buildPreviewItem(name, "MCP", meta, "mcp")); } else if (looksLikeOpencodeShape(parsed)) { opencode = cloneRecord(parsed); preview.push(buildConfigPreview(file, "OpenCode config")); preview.push(...collectOpencodePreviewItems(parsed)); configCount += 1; } else if (looksLikeOpenworkShape(parsed)) { openwork = cloneRecord(parsed); preview.push(buildConfigPreview(file, "OpenWork config")); configCount += 1; } else if (looksLikeMcpShape(parsed)) { const name = resolveName(parsed.name, stem(file.path)); opencode = { mcp: { [name]: cloneRecord(parsed) } }; preview.push(buildPreviewItem(maybeString(parsed.title).trim() || name, "MCP", "MCP config", "mcp")); } else if (looksLikeNamedAgentJsonFile(file)) { const name = resolveName(parsed.name, stem(file.path)); opencode = { agent: { [name]: cloneRecord(parsed) } }; const meta = typeof parsed.model === "string" ? parsed.model : "Agent config"; preview.push(buildPreviewItem(name, "Agent", meta, "agent")); } else { const name = resolveName("", stem(file.path)); config[name] = cloneRecord(parsed); preview.push(buildPreviewItem(basename(file.path) || name, "Config", "Config file", "config")); configCount += 1; } } const secretHits = [ ...redactSecrets(opencode ?? {}, ["opencode"]), ...redactSecrets(openwork ?? {}, ["openwork"]), ...redactSecrets(config, ["config"]), ]; if (secretHits.length) { warnings.push(`Redacted ${secretHits.length} potential secret${secretHits.length === 1 ? "" : "s"} in ${file.path}: ${secretHits.slice(0, 3).join(", ")}`); } if (!opencode && !openwork && !Object.keys(config).length) { warnings.push(`Ignored unsupported config file: ${file.path}`); return null; } return { opencode, openwork, config, preview, configCount }; } function mergeNamedObjects(target: Record, source: Record | undefined): void { for (const [name, value] of Object.entries(source ?? {})) { target[name] = value; } } function buildSummary( skills: Record[], agents: Record, mcp: Record, commands: Record[], warnings: string[], configCount: number, ): PackageSummary { return { skills: skills.length, agents: Object.keys(agents).length, mcpServers: Object.keys(mcp).length, commands: commands.length, configs: configCount, warnings: warnings.length, }; } function buildBundleName( inputName: unknown, skills: { name: string }[], agentCount: number, mcpCount: number, commandCount: number, configCount: number, ): string { const explicit = String(inputName ?? "").trim(); if (explicit) return explicit; if (skills.length === 1 && !agentCount && !mcpCount && !commandCount && !configCount) return skills[0].name; if (skills.length > 1 && !agentCount && !mcpCount && !commandCount && !configCount) return "Shared skills set"; if (skills.length === 1) return `${skills[0].name} worker package`; return "Packaged worker"; } function buildBundleDescription(bundleType: string, summary: PackageSummary): string { if (bundleType === "skill") return "Single skill bundle generated from dropped markdown."; if (bundleType === "skills-set") return `${summary.skills} skills packaged together.`; const parts: string[] = []; if (summary.skills) parts.push(`${summary.skills} skill${summary.skills === 1 ? "" : "s"}`); if (summary.agents) parts.push(`${summary.agents} agent${summary.agents === 1 ? "" : "s"}`); if (summary.mcpServers) parts.push(`${summary.mcpServers} MCP${summary.mcpServers === 1 ? "" : "s"}`); if (summary.commands) parts.push(`${summary.commands} command${summary.commands === 1 ? "" : "s"}`); if (summary.configs) parts.push(`${summary.configs} config${summary.configs === 1 ? "" : "s"}`); return parts.length ? `${parts.join(", ")} bundled into a worker package.` : "Worker package generated from shareable files."; } export function packageOpenworkFiles(input: PackageInput): PackageResult { const rawFiles = Array.isArray(input?.files) ? input.files : []; if (!rawFiles.length) { throw new Error("Drop one or more OpenWork files to package them."); } if (rawFiles.length > MAX_FILES) { throw new Error(`Too many files. Package up to ${MAX_FILES} files at once.`); } const warnings: string[] = []; const skills: SkillRecord[] = []; const commands: CommandRecord[] = []; const previewItems: PreviewItem[] = []; const opencodeAgent: Record = {}; const opencodeMcp: Record = {}; let opencodeConfig: Record | null = null; const genericConfig: Record = {}; let openwork: Record | null = null; let configCount = 0; for (let index = 0; index < rawFiles.length; index += 1) { const file = normalizeFile(rawFiles[index], index); if (!file.content.trim()) { warnings.push(`Ignored empty file: ${file.path}`); continue; } if (isMarkdownFile(file)) { const frontmatter = parseFrontmatter(file.content); const markdownKind = isCommandFile(file) ? "command" : isSkillFile(file) ? "skill" : isAgentFile(file, frontmatter.data) ? "agent" : inferMarkdownKind(file, frontmatter.data); if (markdownKind === "skill") { const record = buildSkillRecord(file, warnings, frontmatter); if (record) { skills.push(record); previewItems.push(record.preview); } continue; } if (markdownKind === "agent") { const record = buildAgentRecord(file, warnings, frontmatter); if (record) { opencodeAgent[record.name] = record.config; previewItems.push(record.preview); } continue; } if (markdownKind === "command") { const record = buildCommandRecord(file, warnings, frontmatter); if (record) { commands.push(record); previewItems.push(record.preview); } continue; } warnings.push(`Ignored unsupported markdown file: ${file.path}`); continue; } if (isJsonFile(file)) { const record = readConfigSection(file, warnings); if (!record) continue; if (record.opencode) { opencodeConfig = mergeConfigSections(opencodeConfig, record.opencode); } mergeNamedObjects(opencodeAgent, (record.opencode?.agent ?? {}) as Record); mergeNamedObjects(opencodeMcp, (record.opencode?.mcp ?? {}) as Record); if (record.openwork) openwork = record.openwork; mergeNamedObjects(genericConfig, record.config ?? {}); configCount += record.configCount ?? 0; previewItems.push(...record.preview); continue; } warnings.push(`Ignored unsupported file type: ${file.path}`); } const cleanSkills = skills.map(({ preview: _preview, ...skill }) => skill); const cleanCommands = commands.map(({ preview: _preview, ...command }) => command); const summary = buildSummary(cleanSkills, opencodeAgent, opencodeMcp, cleanCommands, warnings, configCount); if (!summary.skills && !summary.agents && !summary.mcpServers && !summary.commands && !summary.configs && !openwork) { throw new Error("No shareable files found. Drop markdown or JSON/JSONC config files to package them."); } const hasWorkspaceConfig = Boolean(openwork) || Boolean(opencodeConfig) || Object.keys(opencodeAgent).length || Object.keys(opencodeMcp).length || Object.keys(genericConfig).length || cleanCommands.length; const name = buildBundleName(input?.bundleName, cleanSkills, summary.agents, summary.mcpServers, summary.commands, summary.configs); let bundleType = "workspace-profile"; let bundle: Record; if (cleanSkills.length === 1 && !hasWorkspaceConfig) { bundleType = "skill"; const skill = cleanSkills[0]; bundle = { schemaVersion: 1, type: "skill", name: skill.name, description: skill.description || undefined, trigger: skill.trigger || undefined, content: skill.content, }; } else if (cleanSkills.length > 0 && !hasWorkspaceConfig) { bundleType = "skills-set"; bundle = { schemaVersion: 1, type: "skills-set", name, description: buildBundleDescription("skills-set", summary), skills: cleanSkills, }; } else { const workspace: Record = { workspaceId: "share-service-package", exportedAt: Date.now(), ...(cleanSkills.length ? { skills: cleanSkills } : null), ...(cleanCommands.length ? { commands: cleanCommands } : null), ...(Object.keys(genericConfig).length ? { config: genericConfig } : null), ...(openwork ? { openwork } : null), ...((opencodeConfig || Object.keys(opencodeAgent).length || Object.keys(opencodeMcp).length) ? { opencode: mergeConfigSections(opencodeConfig, { ...(Object.keys(opencodeAgent).length ? { agent: opencodeAgent } : null), ...(Object.keys(opencodeMcp).length ? { mcp: opencodeMcp } : null), }), } : null), }; bundle = { schemaVersion: 1, type: "workspace-profile", name, description: buildBundleDescription("workspace-profile", summary), workspace, }; } return { bundle, bundleType, name: bundle.name as string, summary, warnings, items: previewItems.slice(0, 12), }; }