mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
fix: preserve alwaysApply skill sync alias
This commit is contained in:
parent
a4e51ecc7d
commit
a7d1fce255
3 changed files with 47 additions and 9 deletions
|
|
@ -37,6 +37,10 @@ function getCaseInsensitive(frontmatter: Record<string, unknown>, key: string):
|
|||
return entry?.[1];
|
||||
}
|
||||
|
||||
function hasCaseInsensitive(frontmatter: Record<string, unknown>, key: string): boolean {
|
||||
return Object.keys(frontmatter).some((candidate) => candidate.toLowerCase() === key);
|
||||
}
|
||||
|
||||
function getRawFrontmatterValue(block: string, key: string): string | undefined {
|
||||
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(`^\\s*${escapedKey}\\s*:\\s*(.*)$`, 'i');
|
||||
|
|
@ -62,7 +66,8 @@ function stripInlineComment(value: string): string {
|
|||
|
||||
function normalizeFrontmatterKeys(frontmatter: Record<string, unknown>): Record<string, unknown> {
|
||||
return Object.entries(frontmatter).reduce<Record<string, unknown>>((acc, [key, value]) => {
|
||||
acc[key.toLowerCase()] = value;
|
||||
const normalizedKey = key.toLowerCase();
|
||||
acc[normalizedKey === 'alwaysapply' ? 'alwaysApply' : normalizedKey] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
|
@ -119,8 +124,12 @@ export function parseSkillMarkdown(raw: string): ParsedSkillMarkdown {
|
|||
const nameValue = getCaseInsensitive(frontmatter, 'name');
|
||||
const descriptionValue = getCaseInsensitive(frontmatter, 'description');
|
||||
const whenToUseValue = getCaseInsensitive(frontmatter, 'when-to-use');
|
||||
const alwaysApplyValue = getCaseInsensitive(frontmatter, 'always-apply');
|
||||
const rawAlwaysApplyValue = getRawFrontmatterValue(block, 'always-apply');
|
||||
const hasCanonicalAlwaysApply = hasCaseInsensitive(frontmatter, 'always-apply');
|
||||
const hasAliasAlwaysApply = hasCaseInsensitive(frontmatter, 'alwaysapply');
|
||||
const canonicalAlwaysApplyValue = getCaseInsensitive(frontmatter, 'always-apply');
|
||||
const aliasAlwaysApplyValue = getCaseInsensitive(frontmatter, 'alwaysapply');
|
||||
const rawCanonicalAlwaysApplyValue = getRawFrontmatterValue(block, 'always-apply');
|
||||
const rawAliasAlwaysApplyValue = getRawFrontmatterValue(block, 'alwaysApply');
|
||||
const name = toScalarString(nameValue);
|
||||
let description = '';
|
||||
if (descriptionValue !== undefined) {
|
||||
|
|
@ -130,11 +139,16 @@ export function parseSkillMarkdown(raw: string): ParsedSkillMarkdown {
|
|||
}
|
||||
let alwaysApply: boolean | undefined;
|
||||
const invalidBooleans: string[] = [];
|
||||
if (alwaysApplyValue !== undefined) {
|
||||
alwaysApply = parseBoolean(alwaysApplyValue, rawAlwaysApplyValue);
|
||||
if (alwaysApply === undefined && !hasBooleanPlaceholder(rawAlwaysApplyValue)) {
|
||||
if (hasCanonicalAlwaysApply) {
|
||||
alwaysApply = parseBoolean(canonicalAlwaysApplyValue, rawCanonicalAlwaysApplyValue);
|
||||
if (alwaysApply === undefined && !hasBooleanPlaceholder(rawCanonicalAlwaysApplyValue)) {
|
||||
invalidBooleans.push('always-apply');
|
||||
}
|
||||
} else if (hasAliasAlwaysApply) {
|
||||
alwaysApply = parseBoolean(aliasAlwaysApplyValue, rawAliasAlwaysApplyValue);
|
||||
if (alwaysApply === undefined && !hasBooleanPlaceholder(rawAliasAlwaysApplyValue)) {
|
||||
invalidBooleans.push('alwaysApply');
|
||||
}
|
||||
}
|
||||
return {
|
||||
name,
|
||||
|
|
|
|||
|
|
@ -288,6 +288,24 @@ describe('createGitHubSkillSyncRunner', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('drops an invalid alwaysApply alias when canonical always-apply is valid', async () => {
|
||||
const deps = createDeps({
|
||||
fetchFn: githubFetch(
|
||||
'---\nname: research\ndescription: Research things\nalways-apply: true\nalwaysApply: yes\n---\nBody',
|
||||
),
|
||||
});
|
||||
const runner = createGitHubSkillSyncRunner(deps);
|
||||
const result = await runner.runOnce();
|
||||
|
||||
expect(result.status).toBe('completed');
|
||||
expect(deps.createSkill).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
alwaysApply: true,
|
||||
frontmatter: { 'always-apply': true },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('discovers nested skill roots within the configured discovery depth', async () => {
|
||||
const skillMarkdown = '---\nname: tdd\ndescription: Test-driven development\n---\nBody';
|
||||
const fetchFn = jest.fn(async (input: RequestInfo | URL) => {
|
||||
|
|
|
|||
|
|
@ -317,12 +317,18 @@ function toCleanFrontmatter(
|
|||
delete clean.name;
|
||||
delete clean.description;
|
||||
// Drop a placeholder/non-boolean always-apply (e.g. `always-apply:` or
|
||||
// `always-apply: # TODO`, which js-yaml yields as null). The boolean is
|
||||
// already captured in the dedicated alwaysApply field, and persisting a
|
||||
// null here would leave ambiguous/invalid frontmatter on the synced skill.
|
||||
// `always-apply: # TODO`, which js-yaml yields as null). Apply the same
|
||||
// cleanup to the accepted `alwaysApply` alias so a malformed alias does not
|
||||
// survive after the canonical key has already supplied the effective flag.
|
||||
// The boolean is already captured in the dedicated alwaysApply field, and
|
||||
// persisting a null here would leave ambiguous/invalid frontmatter on the
|
||||
// synced skill.
|
||||
if ('always-apply' in clean && typeof clean['always-apply'] !== 'boolean') {
|
||||
delete clean['always-apply'];
|
||||
}
|
||||
if ('alwaysApply' in clean && typeof clean.alwaysApply !== 'boolean') {
|
||||
delete clean.alwaysApply;
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue