@kya-os/cli
Version:
CLI for MCP-I setup and management
112 lines • 4.65 kB
JavaScript
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