feat: add native Qwen Code hook integration

This commit is contained in:
ZacharyZhang-NY committed 2026-07-19 12:17:04 -04:00
1 parent 3999ed0f44
commit d59b0101b1
12 files changed
+1637

No files matched your search

+237
View File
@@ -0,0 +1,237 @@
import {
applyEdits,
modify,
parse,
parseTree,
printParseErrorCode,
type FormattingOptions,
type JSONPath,
type Node,
type ParseError,
} from "jsonc-parser";
import {
TOOL_EVENTS,
type JsonObject,
type QwenGroup,
type QwenHookSettings,
type ToolEvent,
isObject,
managedRemovals,
readHooks,
} from "./qwen-contract.js";
export const OWNED_FILE_MARKER = "// Managed by Elydora";
export interface QwenDocument {
readonly filePath: string;
readonly exists: boolean;
readonly raw: string;
readonly root: JsonObject;
readonly hooks: QwenHookSettings;
readonly hasHooksContainer: boolean;
readonly hooksDisabled: boolean;
readonly ownedFile: boolean;
}
export interface RenderedDocument {
readonly document: QwenDocument;
readonly changed: boolean;
readonly next?: string;
}
interface DocumentOptions {
readonly exists: boolean;
readonly filePath: string;
readonly raw: string;
}
function rejectDuplicateKeys(
node: Node | undefined,
label: string,
path: string[] = [],
): void {
if (!node) return;
if (node.type === "object") {
const keys = new Set<string>();
for (const property of node.children ?? []) {
const key = String(property.children?.[0]?.value);
const location = [...path, key];
if (keys.has(key))
throw new Error(
`${label} contains duplicate field "${location.join(".")}"`,
);
keys.add(key);
rejectDuplicateKeys(property.children?.[1], label, location);
}
return;
}
if (node.type === "array") {
for (const child of node.children ?? [])
rejectDuplicateKeys(child, label, path);
}
}
function parseJsonWithComments(raw: string, label: string): JsonObject {
const errors: ParseError[] = [];
const value: unknown = parse(raw, errors, {
allowTrailingComma: false,
disallowComments: false,
});
if (errors.length > 0) {
const details = errors
.map(
(error) =>
`${printParseErrorCode(error.error)} at offset ${error.offset}`,
)
.join(", ");
throw new Error(`Failed to parse ${label}: ${details}`);
}
if (!isObject(value)) throw new Error(`${label} must contain a JSON object`);
rejectDuplicateKeys(
parseTree(raw, [], {
allowTrailingComma: false,
disallowComments: false,
}),
label,
);
return value;
}
export function parseDocument(options: DocumentOptions): QwenDocument {
const label = `Qwen Code settings at ${options.filePath}`;
const root = parseJsonWithComments(options.raw, label);
if (
root.disableAllHooks !== undefined &&
typeof root.disableAllHooks !== "boolean"
) {
throw new Error(`${label} field "disableAllHooks" must be a boolean`);
}
const hasHooksContainer = Object.hasOwn(root, "hooks");
const hooks = hasHooksContainer
? readHooks(root.hooks, `${label} field "hooks"`)
: {};
return {
...options,
root,
hooks,
hasHooksContainer,
hooksDisabled: root.disableAllHooks === true,
ownedFile: options.raw.startsWith(OWNED_FILE_MARKER),
};
}
export function createOwnedDocument(filePath: string): QwenDocument {
return parseDocument({
exists: false,
filePath,
raw: `${OWNED_FILE_MARKER}\n{}\n`,
});
}
function formatting(raw: string): FormattingOptions {
const indentation = /\r?\n([ \t]+)\S/.exec(raw)?.[1];
const insertSpaces = !indentation?.includes("\t");
return {
eol: raw.includes("\r\n") ? "\r\n" : "\n",
insertSpaces,
tabSize: insertSpaces ? Math.max(1, indentation?.length ?? 2) : 1,
};
}
function change(
raw: string,
jsonPath: JSONPath,
value: unknown,
isArrayInsertion = false,
): string {
return applyEdits(
raw,
modify(raw, jsonPath, value, {
formattingOptions: formatting(raw),
isArrayInsertion,
}),
);
}
function currentDocument(document: QwenDocument, raw: string): QwenDocument {
return parseDocument({
exists: document.exists,
filePath: document.filePath,
raw,
});
}
function removeManagedEntries(
document: QwenDocument,
raw: string,
agentId?: string,
): string {
const removals = managedRemovals(document.hooks, agentId);
for (const event of TOOL_EVENTS) {
const eventRemovals = removals
.filter((removal) => removal.event === event)
.sort((left, right) => right.groupIndex - left.groupIndex);
for (const removal of eventRemovals) {
const groupPath: JSONPath = ["hooks", event, removal.groupIndex];
if (removal.removeGroup) {
raw = change(raw, groupPath, undefined);
continue;
}
for (const handlerIndex of [...removal.handlerIndexes].sort(
(left, right) => right - left,
)) {
raw = change(raw, [...groupPath, "hooks", handlerIndex], undefined);
}
}
if (eventRemovals.length > 0) {
const current = currentDocument(document, raw);
if ((current.hooks[event] ?? []).length === 0) {
raw = change(raw, ["hooks", event], undefined);
}
}
}
const current = currentDocument(document, raw);
if (current.hasHooksContainer && Object.keys(current.hooks).length === 0) {
raw = change(raw, ["hooks"], undefined);
}
return raw;
}
function appendGroup(
document: QwenDocument,
raw: string,
event: ToolEvent,
group: QwenGroup,
): string {
const current = currentDocument(document, raw);
if (current.hooks[event]) {
return change(
raw,
["hooks", event, current.hooks[event]!.length],
group,
true,
);
}
return change(raw, ["hooks", event], [group]);
}
export function renderDocument(
document: QwenDocument,
agentId: string | undefined,
additions: ReadonlyMap<ToolEvent, QwenGroup>,
): RenderedDocument {
let raw = removeManagedEntries(document, document.raw, agentId);
for (const event of TOOL_EVENTS) {
const group = additions.get(event);
if (group) raw = appendGroup(document, raw, event, group);
}
const current = currentDocument(document, raw);
if (
additions.size === 0 &&
document.ownedFile &&
Object.keys(current.root).length === 0
) {
return { document, changed: true, next: undefined };
}
return { document, changed: raw !== document.raw, next: raw };
}