Files
playbook/antigravity-awesome-skills/tools/lib/aas-v1/adapters/claude.js
T
2026-07-18 00:02:59 +00:00

178 lines
6.6 KiB
JavaScript

"use strict";
const { hostConfigError } = require("./errors");
const KNOWN_KEYS = ["command", "args", "env"];
function decodeUtf8(bytes) {
if (bytes.length > 1024 * 1024) throw hostConfigError("AAS_ADAPTER_CONFIG_TOO_LARGE", "invalidInput");
try {
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
if (text.charCodeAt(0) === 0xfeff) throw new Error("bom");
return text;
} catch {
throw hostConfigError("AAS_ADAPTER_CONFIG_ENCODING_INVALID", "invalidInput");
}
}
function parseJsonAst(text) {
let cursor = 0;
function fail() { throw hostConfigError("AAS_ADAPTER_CLAUDE_JSON_INVALID", "invalidInput"); }
function whitespace() { while (/[\u0009\u000a\u000d\u0020]/.test(text[cursor] || "")) cursor += 1; }
function string() {
const start = cursor;
if (text[cursor] !== '"') fail();
cursor += 1;
while (cursor < text.length) {
if (text[cursor] === '"') {
cursor += 1;
try { return { value: JSON.parse(text.slice(start, cursor)), start, end: cursor }; } catch { fail(); }
}
if (text[cursor] === "\\") cursor += 2;
else cursor += 1;
}
fail();
}
function value(depth = 0) {
if (depth > 32) fail();
whitespace();
const start = cursor;
if (text[cursor] === "{") return object(depth + 1);
if (text[cursor] === "[") return array(depth + 1);
if (text[cursor] === '"') {
const node = string();
return { type: "string", ...node };
}
while (cursor < text.length && !/[\s,}\]]/.test(text[cursor])) cursor += 1;
if (cursor === start) fail();
try { return { type: "primitive", value: JSON.parse(text.slice(start, cursor)), start, end: cursor }; } catch { fail(); }
}
function array(depth) {
const start = cursor;
const items = [];
cursor += 1;
whitespace();
if (text[cursor] === "]") { cursor += 1; return { type: "array", value: [], items, start, end: cursor }; }
while (cursor < text.length) {
const item = value(depth);
items.push(item);
whitespace();
if (text[cursor] === "]") { cursor += 1; return { type: "array", value: items.map((entry) => entry.value), items, start, end: cursor }; }
if (text[cursor] !== ",") fail();
cursor += 1;
}
fail();
}
function object(depth) {
const start = cursor;
const properties = [];
const keys = new Set();
const result = {};
cursor += 1;
whitespace();
if (text[cursor] === "}") { cursor += 1; return { type: "object", value: result, properties, start, end: cursor }; }
while (cursor < text.length) {
whitespace();
const key = string();
if (keys.has(key.value)) throw hostConfigError("AAS_ADAPTER_CLAUDE_JSON_DUPLICATE_KEY", "invalidInput", { key: key.value });
keys.add(key.value);
whitespace();
if (text[cursor] !== ":") fail();
cursor += 1;
const child = value(depth);
properties.push({ key: key.value, keyNode: key, valueNode: child });
result[key.value] = child.value;
whitespace();
if (text[cursor] === "}") { cursor += 1; return { type: "object", value: result, properties, start, end: cursor }; }
if (text[cursor] !== ",") fail();
cursor += 1;
}
fail();
}
const root = value(0);
whitespace();
if (cursor !== text.length || root.type !== "object") fail();
return root;
}
function property(node, key) {
return node.properties.find((entry) => entry.key === key);
}
function indentation(text) {
const match = text.match(/\n([ \t]+)"/);
return match ? match[1] : " ";
}
function lineIndent(text, offset) {
const lineStart = text.lastIndexOf("\n", offset - 1) + 1;
return (text.slice(lineStart, offset).match(/^[ \t]*/) || [""])[0];
}
function formatValue(value, continuationIndent, indentUnit) {
return JSON.stringify(value, null, indentUnit).split("\n").map((line, index) => index === 0 ? line : `${continuationIndent}${line}`).join("\n");
}
function replaceRange(text, start, end, replacement) {
return `${text.slice(0, start)}${replacement}${text.slice(end)}`;
}
function insertProperty(text, node, key, value, indentUnit) {
const baseIndent = lineIndent(text, node.start);
const childIndent = `${baseIndent}${indentUnit}`;
const rendered = `${JSON.stringify(key)}: ${formatValue(value, childIndent, indentUnit)}`;
if (node.properties.length === 0) {
return replaceRange(text, node.start, node.end, `{\n${childIndent}${rendered}\n${baseIndent}}`);
}
const last = node.properties.at(-1).valueNode;
return replaceRange(text, last.end, last.end, `,\n${childIndent}${rendered}`);
}
function sameValue(left, right) {
return JSON.stringify(left) === JSON.stringify(right);
}
function inspectTree(root) {
const mcp = property(root, "mcpServers");
if (mcp && mcp.valueNode.type !== "object") throw hostConfigError("AAS_ADAPTER_CLAUDE_SECTION_AMBIGUOUS", "invalidInput", { key: "mcpServers" });
const aas = mcp ? property(mcp.valueNode, "aas") : null;
if (aas && aas.valueNode.type !== "object") throw hostConfigError("AAS_ADAPTER_CLAUDE_SECTION_AMBIGUOUS", "invalidInput", { key: "aas" });
return {
mcp: mcp ? mcp.valueNode : null,
aas: aas ? aas.valueNode : null,
inspection: {
sectionPresent: Boolean(aas),
configured: Boolean(aas) && KNOWN_KEYS.every((key) => Object.hasOwn(aas.valueNode.value, key)),
unknownKeys: aas ? Object.keys(aas.valueNode.value).filter((key) => !KNOWN_KEYS.includes(key)).sort() : [],
},
};
}
function buildClaudeText(bytes, server) {
const source = decodeUtf8(bytes);
const text = source.trim() === "" ? "{}\n" : source;
const root = parseJsonAst(text);
const { mcp, aas, inspection } = inspectTree(root);
const changedPaths = KNOWN_KEYS.filter((key) => !aas || !Object.hasOwn(aas.value, key) || !sameValue(aas.value[key], server[key])).map((key) => `mcpServers.aas.${key}`);
if (changedPaths.length === 0) return { text, inspection, changedPaths };
const indentUnit = indentation(text);
let nextText;
if (aas) {
const nextAas = { ...aas.value, command: server.command, args: server.args, env: server.env };
nextText = replaceRange(text, aas.start, aas.end, formatValue(nextAas, lineIndent(text, aas.start), indentUnit));
} else if (mcp) {
nextText = insertProperty(text, mcp, "aas", server, indentUnit);
} else {
nextText = insertProperty(text, root, "mcpServers", { aas: server }, indentUnit);
}
return { text: nextText, inspection, changedPaths };
}
function inspectClaudeBytes(bytes) {
const source = decodeUtf8(bytes);
const root = parseJsonAst(source.trim() === "" ? "{}" : source);
return inspectTree(root).inspection;
}
module.exports = { buildClaudeText, inspectClaudeBytes, parseJsonAst };