Hermes Agent writes ~/.hermes/config.yaml with PyYAML's default block
style, which puts list items at the SAME indent as the parent key:
platform_toolsets:
cli:
- hermes-cli # indent 2, same as `cli:`
- browser
The previous line-based YAML patcher used `^ \S` to find the end of
the `cli:` block, which mistook that first ` - hermes-cli` line for
the next sibling key, truncated the block, and spliced
` - mcp-codegraph` at indent 4 BEFORE the existing items. The
result was unparseable YAML: every subsequent item (`- browser`,
`- clarify`, …) and every sibling platform (`telegram:`, `discord:`)
appeared at the `platform_toolsets:` level. Hermes silently fell back
to the default config, dropping every user override.
The new `listChildBlock` helper recognizes ` - ` as a list-item
continuation (not a sibling key), finds the real end of the block at
the next sibling mapping key, and detects the existing item indent so
the new entry matches it. Two regression tests cover the PyYAML-default
style; the existing 4-space-nested test still passes.
End-to-end verified against a real `hermes-agent` install on the exact
bug-triggering config: `hermes mcp list` shows codegraph as enabled,
`hermes tools --summary` lists both `mcp-codegraph` and `codegraph` in
the CLI toolset, and `hermes mcp test codegraph` connects in 264ms and
discovers all 10 codegraph tools. Re-running `codegraph install`
reports `Unchanged` and the file still has exactly one entry. Closes #456.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
8876defbc1
commit
e76cc547b0
@@ -188,6 +188,63 @@ function childRange(lines: string[], parent: LineRange, child: string): LineRang
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
/**
|
||||
* Block-range for a 2-space-indented child whose value is a YAML block list.
|
||||
*
|
||||
* Unlike `childRange`, this handles PyYAML's default `default_flow_style=False`
|
||||
* serialization, where list items sit at the SAME indent as the parent key:
|
||||
*
|
||||
* cli:
|
||||
* - hermes-cli # indent 2 — belongs to cli, not a sibling
|
||||
* - browser
|
||||
*
|
||||
* `childRange`'s `^ \S` heuristic mistakes that first ` - hermes-cli` line
|
||||
* for the next sibling key and truncates the block, causing inserts to land
|
||||
* before the existing items at a different indent (issue #456). This helper
|
||||
* recognizes a ` - ` line as part of the block instead, and reports back
|
||||
* the actual indent used by existing items so the inserter matches it.
|
||||
*/
|
||||
function listChildBlock(
|
||||
lines: string[],
|
||||
parent: LineRange,
|
||||
child: string,
|
||||
): (LineRange & { itemIndent: string }) | null {
|
||||
const startPattern = new RegExp(`^ ${escapeRegExp(child)}:\\s*(?:#.*)?$`);
|
||||
let start = -1;
|
||||
for (let i = parent.start + 1; i < parent.end; i++) {
|
||||
if (startPattern.test(lines[i] ?? '')) {
|
||||
start = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (start === -1) return null;
|
||||
|
||||
let end = parent.end;
|
||||
for (let i = start + 1; i < parent.end; i++) {
|
||||
const line = lines[i] ?? '';
|
||||
if (line.trim() === '') continue;
|
||||
const indentMatch = line.match(/^( *)/);
|
||||
const indent = indentMatch?.[1]?.length ?? 0;
|
||||
if (indent >= 4) continue;
|
||||
if (indent === 2 && /^ - /.test(line)) continue;
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
while (end > start + 1 && (lines[end - 1] ?? '').trim() === '') {
|
||||
end--;
|
||||
}
|
||||
|
||||
let itemIndent = ' ';
|
||||
for (let i = start + 1; i < end; i++) {
|
||||
const m = (lines[i] ?? '').match(/^( +)- /);
|
||||
if (m && m[1]) {
|
||||
itemIndent = m[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { start, end, itemIndent };
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
@@ -251,7 +308,7 @@ function removeCodeGraphMcpServer(content: string): string {
|
||||
function upsertCodeGraphToolset(content: string): string {
|
||||
const lines = splitLines(content);
|
||||
const parent = topLevelRange(lines, 'platform_toolsets');
|
||||
const cli = parent ? childRange(lines, parent, 'cli') : null;
|
||||
const cli = parent ? listChildBlock(lines, parent, 'cli') : null;
|
||||
|
||||
if (!parent) {
|
||||
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
||||
@@ -270,14 +327,14 @@ function upsertCodeGraphToolset(content: string): string {
|
||||
.some((line) => line.trim() === '- mcp-codegraph');
|
||||
if (hasEntry) return joinLines(lines);
|
||||
|
||||
lines.splice(cli.end, 0, ' - mcp-codegraph');
|
||||
lines.splice(cli.end, 0, `${cli.itemIndent}- mcp-codegraph`);
|
||||
return joinLines(lines);
|
||||
}
|
||||
|
||||
function removeCodeGraphToolset(content: string): string {
|
||||
const lines = splitLines(content);
|
||||
const parent = topLevelRange(lines, 'platform_toolsets');
|
||||
const cli = parent ? childRange(lines, parent, 'cli') : null;
|
||||
const cli = parent ? listChildBlock(lines, parent, 'cli') : null;
|
||||
if (!cli) return content;
|
||||
|
||||
const hasEntry = lines
|
||||
|
||||
Reference in New Issue
Block a user