178 lines
6.6 KiB
JavaScript
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 };
|