UNPKG

@kya-os/cli

Version:

CLI for MCP-I setup and management

112 lines 4.65 kB
import { existsSync, mkdirSync, readFileSync } from "fs"; import { join } from "path"; import { writeFileAtomicSafe } from "./fs-safe.js"; /** * Claude Code integration: a managed block in CLAUDE.md instructs the agent * harness to attribute PRs to the registered agent, and .claude/settings.json * disables the stock "Generated with Claude Code" attribution. * * The PR byline can only be influenced through instructions because the PR * body is composed by the model at PR time; commit trailers are enforced * mechanically by the git hook instead (see git-config.ts). */ export const CLAUDE_MD_START = "<!-- mcpi:dco:start -->"; export const CLAUDE_MD_END = "<!-- mcpi:dco:end -->"; /** * Values interpolated into the managed block come from .mcpi/agent.json and * git config, both attacker-influenceable in a cloned repo. Clamp everything * to a single line with no control characters so crafted metadata cannot * inject extra instructions into a block the harness treats as trusted. */ function singleLine(value) { // eslint-disable-next-line no-control-regex return value.replace(/[\u0000-\u001f\u007f]+/g, " ").trim(); } export function buildAttributionBlock(params) { const agentName = singleLine(params.agentName); const agentUrl = singleLine(params.agentUrl); const lines = [ CLAUDE_MD_START, "## Agent attribution (managed by `mcpi dco setup`)", "", "- When creating pull requests, end the PR description with exactly this byline:", ` \`Generated with [${agentName}](${agentUrl})\``, '- Never add "Generated with Claude Code" or any other tool byline to PR descriptions or commit messages.', ]; if (params.includeTrailerInstructions) { lines.push("- End every commit message with these trailers:", ` \`Signed-off-by: ${singleLine(params.humanName ?? "")} <${singleLine(params.humanEmail ?? "")}>\``, ` \`Co-authored-by: ${agentName} <${singleLine(params.agentEmail ?? "")}>\``); } lines.push(CLAUDE_MD_END); return lines.join("\n"); } export function upsertClaudeMdBlock(cwd, block, options = {}) { const path = join(cwd, "CLAUDE.md"); if (!existsSync(path)) { if (options.dryRun) { return { result: "would-change", path }; } writeFileAtomicSafe(path, block + "\n"); return { result: "created", path }; } const existing = readFileSync(path, "utf-8"); const startIdx = existing.indexOf(CLAUDE_MD_START); const endIdx = existing.indexOf(CLAUDE_MD_END); let next; if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) { const before = existing.slice(0, startIdx); const after = existing.slice(endIdx + CLAUDE_MD_END.length); next = before + block + after; } else { const separator = existing.endsWith("\n") ? "\n" : "\n\n"; next = existing + separator + block + "\n"; } if (next === existing) { return { result: "unchanged", path }; } if (options.dryRun) { return { result: "would-change", path }; } writeFileAtomicSafe(path, next); return { result: "updated", path }; } /** * Set includeCoAuthoredBy: false in .claude/settings.json, preserving every * other key. A file we cannot parse is left untouched: clobbering a user's * settings is worse than leaving the stock byline on. */ export function updateClaudeSettings(cwd, options = {}) { const dir = join(cwd, ".claude"); const path = join(dir, "settings.json"); let settings = {}; if (existsSync(path)) { try { const parsed = JSON.parse(readFileSync(path, "utf-8")); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return { result: "skipped-invalid", path }; } settings = parsed; } catch { return { result: "skipped-invalid", path }; } if (settings.includeCoAuthoredBy === false) { return { result: "unchanged", path }; } if (options.dryRun) { return { result: "would-change", path }; } settings.includeCoAuthoredBy = false; writeFileAtomicSafe(path, JSON.stringify(settings, null, 2) + "\n"); return { result: "updated", path }; } if (options.dryRun) { return { result: "would-change", path }; } if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } writeFileAtomicSafe(path, JSON.stringify({ includeCoAuthoredBy: false }, null, 2) + "\n"); return { result: "created", path }; } //# sourceMappingURL=claude-config.js.map