UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

599 lines (526 loc) 21.6 kB
/** * OpenClaw Provider * * OpenClaw uses an MCP-based integration model similar to Hermes, but with * native support for all 5 artifact types including behaviors. * * What this provider deploys (all user-global, home directory): * - Agents: ~/.openclaw/agents/ (native agent definitions) * - Commands: ~/.openclaw/commands/ (slash commands) * - Skills: ~/.openclaw/skills/ (NLP-triggered capabilities) * - Rules: ~/.openclaw/rules/ (context-loaded constraints) * - Behaviors: ~/.openclaw/behaviors/ (reactive capabilities with hooks + scripts) * * OpenClaw is the first platform to support behaviors natively. * Behaviors are a new AIWG artifact type: reactive capabilities with scripts * and event hooks that fire automatically when system events occur. * * See: docs/openclaw-guide.md */ import realFs from 'fs'; import { createRequire } from 'module'; const _require = createRequire(import.meta.url); let fs; try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs; } catch { fs = realFs; } import path from 'path'; import os from 'os'; import { ensureDir, listMdFiles, listMdFilesRecursive, listSkillDirs, deploySkillDir, deploySkillsWithKernelRouting, isKernelSkill, pruneStaleAiwgSkills, computeAllKernelNames, deployFiles, getAddonAgentFiles, getAddonCommandFiles, getAddonSkillDirs, getAddonRuleFiles, assembleRulesIndex, normalizeDeploymentMode, collectFrameworkArtifacts, cleanupOldRuleFiles, filterAgentFiles, filterCommandsAgainstSkills, } from './base.mjs'; // ============================================================================ // Provider Configuration // ============================================================================ export const name = 'openclaw'; export const aliases = []; const openclawHome = path.join(os.homedir(), '.openclaw'); export const paths = { agents: path.join(openclawHome, 'agents'), commands: path.join(openclawHome, 'commands'), // Skills sequestered under ~/.openclaw/.aiwg/skills/ — index-driven discovery (#1212). skills: path.join(openclawHome, '.aiwg', 'skills'), rules: path.join(openclawHome, 'rules'), behaviors: path.join(openclawHome, 'behaviors'), // Per ADR-3 + PUW-008/009: native hook loader path. // BEHAVIOR.md → HOOK.md + handler.js translation lands here. hooks: path.join(openclawHome, 'hooks'), }; // Kernel skills (always-loaded) deploy to the platform-native dir. // Critical for OpenClaw: DEFAULT_MAX_SKILLS_IN_PROMPT = 150, so the // kernel set must come in under that floor (~9 today, well under cap). export const kernelSkillsPath = path.join(openclawHome, 'skills'); export const support = { agents: 'native', commands: 'native', skills: 'native', rules: 'native', behaviors: 'native', }; export const capabilities = { skills: true, rules: true, behaviors: true, aggregatedOutput: false, yamlFormat: false, homeDirectoryDeploy: true, }; // ============================================================================ // Model Mapping (passthrough — OpenClaw uses its own model config) // ============================================================================ export function mapModel(shorthand, modelCfg, modelsConfig) { return shorthand; } // ============================================================================ // Content Transformation (passthrough — OpenClaw uses AIWG native format) // ============================================================================ export function transformAgent(srcPath, content, opts) { return content; } export function transformCommand(srcPath, content, opts) { return content; } // ============================================================================ // Deployment Functions // ============================================================================ /** * Deploy agents to ~/.openclaw/agents/ */ function deployAgents(agentFiles, opts) { ensureDir(paths.agents, opts.dryRun); return deployFiles(agentFiles, paths.agents, { ...opts, injectPlatform: true }, transformAgent); } /** * Deploy commands to ~/.openclaw/commands/ */ function deployCommands(commandFiles, opts) { ensureDir(paths.commands, opts.dryRun); return deployFiles(commandFiles, paths.commands, opts, transformCommand); } /** * Deploy skills with kernel-vs-standard routing (#1212/#1216). * * OpenClaw is the binding constraint for the kernel pivot: * `DEFAULT_MAX_SKILLS_IN_PROMPT = 150` is the hard cap. The kernel set * (one quickref per installed framework + core utility set, ~9 today) * fits comfortably under that floor regardless of how many frameworks * are installed. * * - kernel skills → ~/.openclaw/skills/aiwg/<name> (platform-native, always-loaded) * - standard → ~/.openclaw/.aiwg/skills/<name> (index-discoverable, hidden from flat scan) * * The legacy `aiwg/` 2-level namespacing under the kernel path (PUW-025 * #1126) is preserved to avoid collision with non-AIWG ClaWHub installs. */ function deploySkills(skillDirs, opts) { // Standard: ~/.openclaw/.aiwg/skills/ (no extra aiwg/ — the .aiwg/ segment IS the namespace) const standardDestDir = paths.skills; // Kernel: ~/.openclaw/skills/aiwg/ (preserves legacy 2-level namespacing) const kernelDestDir = path.join(kernelSkillsPath, 'aiwg'); deploySkillsWithKernelRouting(skillDirs, standardDestDir, kernelDestDir, opts); // PUW-012 (#1113): also deploy to project-local .agents/skills/ as a // cross-agent compatibility path. OpenClaw's primary deploy is the // home-dir namespaced layout above; this secondary surface lets other // tools running on the same project pick up the same skills via the // universal `.agents/skills/<name>/SKILL.md` discovery convention. // // Honors #1217 no-copy default: filter to kernel-only unless the // operator opts in via env var so standard skills stay at $AIWG_ROOT // and are reached via `aiwg discover`. const copyStandardSkills = opts?.copyStandardSkills === true; const crossAgentSkills = copyStandardSkills ? skillDirs : skillDirs.filter(d => isKernelSkill(d)); if (crossAgentSkills.length > 0) { const crossAgentDir = path.join(process.cwd(), '.agents', 'skills'); ensureDir(crossAgentDir, opts.dryRun); if (!opts.dryRun) { console.log(`Deploying cross-agent skills to ${path.relative(process.cwd(), crossAgentDir)}...`); } else { console.log(`[dry-run] Would deploy cross-agent skills to .agents/skills/`); } for (const skillDir of crossAgentSkills) { deploySkillDir(skillDir, crossAgentDir, opts); } } } /** * Deploy rules to ~/.openclaw/rules/ */ function deployRules(ruleFiles, opts) { ensureDir(paths.rules, opts.dryRun); cleanupOldRuleFiles(paths.rules, opts); return deployFiles(ruleFiles, paths.rules, opts, transformCommand); } /** * Deploy behaviors to ~/.openclaw/behaviors/ * * Behaviors are directories containing BEHAVIOR.md and a scripts/ subdirectory. * Each behavior directory is copied wholesale to the target. */ function deployBehaviors(behaviorDirs, opts) { ensureDir(paths.behaviors, opts.dryRun); let count = 0; for (const behaviorDir of behaviorDirs) { const behaviorName = path.basename(behaviorDir); const destDir = path.join(paths.behaviors, behaviorName); if (opts.dryRun) { console.log(`[dry-run] Would deploy behavior: ${behaviorName} -> ${destDir}`); count++; continue; } ensureDir(destDir); // Copy BEHAVIOR.md const behaviorMd = path.join(behaviorDir, 'BEHAVIOR.md'); if (fs.existsSync(behaviorMd)) { fs.copyFileSync(behaviorMd, path.join(destDir, 'BEHAVIOR.md')); } // Copy scripts/ directory if it exists const scriptsDir = path.join(behaviorDir, 'scripts'); if (fs.existsSync(scriptsDir)) { const destScriptsDir = path.join(destDir, 'scripts'); ensureDir(destScriptsDir); for (const entry of fs.readdirSync(scriptsDir)) { const srcFile = path.join(scriptsDir, entry); const destFile = path.join(destScriptsDir, entry); if (fs.statSync(srcFile).isFile()) { fs.copyFileSync(srcFile, destFile); // Preserve executable permission for scripts try { fs.chmodSync(destFile, 0o755); } catch { /* ignore on platforms without chmod */ } } } } count++; } // PUW-008/PUW-009 (#1109/#1110) per ADR-3: also emit hook artifacts to // ~/.openclaw/hooks/<name>/ so OpenClaw's native hook loader picks them // up. The behaviors/ writes above keep happening per always-deploy // invariant (operator visibility); hooks/ is the discovery bridge. try { ensureDir(paths.hooks, opts.dryRun); if (!opts.dryRun) { // Lazy import so this is only loaded when behaviors exist. import('./openclaw-translator.mjs').then((mod) => { let translated = 0; const skipped = []; for (const behaviorDir of behaviorDirs) { const behaviorName = path.basename(behaviorDir); const hookDir = path.join(paths.hooks, behaviorName); const r = mod.translateBehaviorToHook(behaviorDir, hookDir, opts); if (r.ok) translated++; else skipped.push({ name: behaviorName, reason: r.reason }); } if (translated > 0) { console.log(`Translated ${translated} behavior(s) to OpenClaw hooks at ${paths.hooks}`); } if (skipped.length > 0 && opts.verbose) { for (const s of skipped) { console.log(`Skipped hook translation for ${s.name}: ${s.reason}`); } } }).catch((err) => { console.warn(`Warning: hook translation failed: ${err.message || err}`); }); } } catch (err) { console.warn(`Warning: hook deployment skipped: ${err.message || err}`); } return count; } // ============================================================================ // Behavior Discovery // ============================================================================ /** * List behavior directories (directories containing BEHAVIOR.md) */ function listBehaviorDirs(dir) { if (!fs.existsSync(dir)) return []; return fs .readdirSync(dir, { withFileTypes: true }) .filter((e) => e.isDirectory() && fs.existsSync(path.join(dir, e.name, 'BEHAVIOR.md'))) .map((e) => path.join(dir, e.name)); } /** * Collect behavior directories from all sources: * - Cross-framework: agentic/code/behaviors/ * - Per-addon: agentic/code/addons/<name>/behaviors/ * - Per-framework: agentic/code/frameworks/<name>/behaviors/ * - Direct component source: <srcRoot>/behaviors/ */ function collectBehaviorDirs(srcRoot, mode) { const dirs = []; const seen = new Set(); const addDirs = (items) => { for (const dir of items) { if (seen.has(dir)) continue; seen.add(dir); dirs.push(dir); } }; // Direct component source, used by addon-only deploys such as: // aiwg use aiwg-fleet --provider openclaw addDirs(listBehaviorDirs(path.join(srcRoot, 'behaviors'))); // Cross-framework behaviors const globalBehaviorsDir = path.join(srcRoot, 'agentic', 'code', 'behaviors'); addDirs(listBehaviorDirs(globalBehaviorsDir)); // Addon behaviors const addonsDir = path.join(srcRoot, 'agentic', 'code', 'addons'); if (fs.existsSync(addonsDir)) { for (const entry of fs.readdirSync(addonsDir, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const addonBehaviorsDir = path.join(addonsDir, entry.name, 'behaviors'); addDirs(listBehaviorDirs(addonBehaviorsDir)); } } // Per-framework behaviors const frameworksDir = path.join(srcRoot, 'agentic', 'code', 'frameworks'); if (fs.existsSync(frameworksDir)) { for (const entry of fs.readdirSync(frameworksDir, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const fwBehaviorsDir = path.join(frameworksDir, entry.name, 'behaviors'); addDirs(listBehaviorDirs(fwBehaviorsDir)); } } return dirs; } // ============================================================================ // File Extension // ============================================================================ export function getFileExtension(type) { return '.md'; } // ============================================================================ // Main Deploy Function // ============================================================================ export async function deploy(opts) { const { srcRoot, mode, deployCommands: shouldDeployCommands, deploySkills: shouldDeploySkills, deployRules: shouldDeployRules, commandsOnly, skillsOnly, rulesOnly, dryRun, } = opts; const verbose = opts.verbose || false; if (!opts.quiet) { console.log(`\n=== OpenClaw Provider ===`); console.log(`Deploy target: ${openclawHome}`); console.log(`Mode: ${mode}`); console.log(`Architecture: OpenClaw -> MCP -> AIWG`); console.log(''); } const normalizedMode = normalizeDeploymentMode(mode); // ── Collect source artifacts ────────────────────────────────────────────── const agentFiles = []; const commandFiles = []; const skillDirs = []; const ruleFiles = []; // Addon artifacts if (normalizedMode === 'general' || normalizedMode === 'sdlc' || normalizedMode === 'both' || normalizedMode === 'all') { agentFiles.push(...getAddonAgentFiles(srcRoot)); if (shouldDeployCommands || commandsOnly) commandFiles.push(...getAddonCommandFiles(srcRoot)); if (shouldDeploySkills || skillsOnly) skillDirs.push(...getAddonSkillDirs(srcRoot)); if (shouldDeployRules || rulesOnly) ruleFiles.push(...getAddonRuleFiles(srcRoot)); } // Framework artifacts const frameworkArtifacts = collectFrameworkArtifacts(srcRoot, normalizedMode, { includeAgents: true, includeCommands: shouldDeployCommands || commandsOnly, includeSkills: shouldDeploySkills || skillsOnly, includeRules: shouldDeployRules || rulesOnly, recursiveCommands: true, consolidatedSdlcRules: true, }); agentFiles.push(...frameworkArtifacts.agents); commandFiles.push(...frameworkArtifacts.commands); skillDirs.push(...frameworkArtifacts.skills); ruleFiles.push(...frameworkArtifacts.rules); // Behaviors (OpenClaw-specific — first provider to support this) const behaviorDirs = collectBehaviorDirs(srcRoot, normalizedMode); // ── Deploy ──────────────────────────────────────────────────────────────── const counts = { agents: 0, commands: 0, skills: 0, rules: 0, behaviors: 0 }; // Agents if (!commandsOnly && !skillsOnly && !rulesOnly) { const filteredAgents = filterAgentFiles(agentFiles, opts); if (verbose) console.log(`\nDeploying ${filteredAgents.length} agents to ${paths.agents}...`); deployAgents(filteredAgents, opts); counts.agents = filteredAgents.length; } // Commands (filter collisions with skills) const filteredCommands = (shouldDeploySkills || skillsOnly) ? filterCommandsAgainstSkills(commandFiles, skillDirs) : commandFiles; if (shouldDeployCommands || commandsOnly) { if (verbose) console.log(`\nDeploying ${filteredCommands.length} commands to ${paths.commands}...`); deployCommands(filteredCommands, opts); counts.commands = filteredCommands.length; } // Skills if (shouldDeploySkills || skillsOnly) { if (verbose) console.log(`\nDeploying ${skillDirs.length} skills to ${paths.skills}/aiwg/ (PUW-025 namespaced)...`); deploySkills(skillDirs, opts); counts.skills = skillDirs.length; // Holistic post-deploy cleanup of stale AIWG-managed kernel // skills (renamed/removed sources). Runs once per deploy with the // global desired-kernel set so it survives multi-call orchestration // (`aiwg use` invokes deploy-agents.mjs once per framework + addon // batch). { const _kernelDestDir = path.isAbsolute(kernelSkillsPath) ? kernelSkillsPath : path.join(target, kernelSkillsPath); pruneStaleAiwgSkills(_kernelDestDir, computeAllKernelNames(srcRoot), opts); } } // Rules if (shouldDeployRules || rulesOnly) { const assembled = assembleRulesIndex(srcRoot); if (assembled) { const { tmpdir } = await import('os'); const tmpDir = fs.mkdtempSync(path.join(tmpdir(), 'aiwg-rules-assembly-')); const assembledPath = path.join(tmpDir, 'RULES-INDEX.md'); fs.writeFileSync(assembledPath, assembled); const finalRuleFiles = [ assembledPath, ...ruleFiles.filter(f => path.basename(f) !== 'RULES-INDEX.md'), ]; if (verbose) console.log(`\nDeploying assembled RULES-INDEX.md + ${finalRuleFiles.length - 1} additional rule files...`); deployRules(finalRuleFiles, opts); counts.rules = finalRuleFiles.length; try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ } } else { if (verbose) console.log(`\nDeploying ${ruleFiles.length} rules to ${paths.rules}...`); deployRules(ruleFiles, opts); counts.rules = ruleFiles.length; } } // Behaviors (always deploy if available — OpenClaw is the first native behaviors platform) if (!commandsOnly && !skillsOnly && !rulesOnly && behaviorDirs.length > 0) { if (verbose) console.log(`\nDeploying ${behaviorDirs.length} behaviors to ${paths.behaviors}...`); counts.behaviors = deployBehaviors(behaviorDirs, opts); } // ── Summary ─────────────────────────────────────────────────────────────── if (verbose) { console.log('\n=== OpenClaw deployment complete ===\n'); } else { const parts = []; if (counts.agents > 0) parts.push(`${counts.agents} agents`); if (counts.commands > 0) parts.push(`${counts.commands} commands`); if (counts.skills > 0) parts.push(`${counts.skills} skills`); if (counts.rules > 0) parts.push(`${counts.rules} rules`); if (counts.behaviors > 0) parts.push(`${counts.behaviors} behaviors`); if (parts.length > 0) { console.log(` Deployed: ${parts.join(' ')}`); } } // ── Post-deployment hint ────────────────────────────────────────────────── if (!opts.quiet) { console.log(''); console.log('All artifacts deployed to ~/.openclaw/ (user-global).'); console.log('Next: configure ~/.openclaw/config.yaml to connect AIWG MCP server.'); console.log('See: docs/openclaw-guide.md'); } } // ============================================================================ // Plugin Bundle Generation (OpenClaw / ClawHub) // ============================================================================ /** * Generate a `clawhub.json` manifest for the ClawHub registry. * * ClawHub is OpenClaw's central package registry. AIWG generates the manifest * alongside file deployment; publishing to ClawHub is a separate workflow * (not yet implemented — publish via ClawHub CLI manually for now). * * @param {string} targetDir - Where to write clawhub.json * @param {{ dryRun?: boolean, srcRoot?: string, name?: string, version?: string, description?: string, tags?: string[] }} opts */ export function generatePluginBundle(targetDir, opts = {}) { const { dryRun = false, srcRoot = process.cwd(), name: pluginName = 'aiwg-plugin', version: overrideVersion, description = 'AIWG plugin for OpenClaw', tags = ['aiwg', 'multi-agent'], } = opts; // Resolve version let version = overrideVersion; if (!version) { try { const pkgPath = path.join(srcRoot, 'package.json'); if (fs.existsSync(pkgPath)) { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); version = pkg.version || 'unknown'; } else { version = 'unknown'; } } catch { version = 'unknown'; } } const manifest = { name: pluginName, version, description, author: 'AIWG Contributors', license: 'MIT', homepage: 'https://aiwg.io', repository: 'https://github.com/jmagly/aiwg', tags, contents: { agents: '~/.openclaw/agents/', commands: '~/.openclaw/commands/', skills: '~/.openclaw/skills/', rules: '~/.openclaw/rules/', behaviors: '~/.openclaw/behaviors/', }, }; const manifestPath = path.join(targetDir, 'clawhub.json'); if (dryRun) { console.log(`[dry-run] Would create ${manifestPath}`); return { manifestPath, manifest }; } if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }); } fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8'); console.log(`Generated OpenClaw ClawHub manifest: ${manifestPath}`); return { manifestPath, manifest }; } // ============================================================================ // Default Export // ============================================================================ export default { name, aliases, paths, kernelSkillsPath, support, capabilities, transformAgent, transformCommand, mapModel, getFileExtension, generatePluginBundle, deploy, };