-
Notifications
You must be signed in to change notification settings - Fork 454
feat: [ENG-2697] rewrite ByteRover skill — sub-skill split + Hermes connector #675
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
7f2143b
Merge pull request #657 from campfirein/feat/ENG-2840
DatPham-6996 019bd3a
Merge remote-tracking branch 'origin/proj/byterover-tool-mode' into f…
DatPham-6996 f5e92f5
feat: [ENG-2697] split ByteRover skill into sub-skills, add Hermes co…
DatPham-6996 090a942
Merge remote-tracking branch 'origin/proj/byterover-tool-mode' into f…
DatPham-6996 eafd498
feat: [ENG-2697] add curate-judgement sub-skill, harden curate guide
DatPham-6996 bdfdcd3
Merge branch 'feat/ENG-2697-variant' into feat/ENG-2697
DatPham-6996 125d2fe
Merge branch 'proj/byterover-tool-mode' into feat/ENG-2697
DatPham-6996 f2bbbf3
fix: [ENG-2697] sub-skill gaps — missing brv commands + examples
cuongdo-byterover cda1cd3
Revert "Merge branch 'feat/ENG-2697-variant' into feat/ENG-2697"
DatPham-6996 cb2cd04
feat: [ENG-2536] add ByteRover onboarding tour skill
wzlng ed5fd7c
Merge pull request #698 from campfirein/feat/ENG-2536
DatPham-6996 2baf272
feat: [ENG-2697] surface vc/sync/cross-agent controls in onboarding tour
DatPham-6996 6733e84
docs: [ENG-2697] correct dream.md sub-skill to match codebase
RyanNg1403 2eec288
Merge branch 'proj/byterover-tool-mode' into feat/ENG-2697
DatPham-6996 b49d341
fix: preserve malformed YAML configs during MCP install
DatPham-6996 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
103 changes: 103 additions & 0 deletions
103
src/server/infra/connectors/mcp/yaml-mcp-config-writer.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| import {dump as yamlDump, load as yamlLoad} from 'js-yaml' | ||
| import {has, set, unset} from 'lodash-es' | ||
|
|
||
| import type {IFileService} from '../../../core/interfaces/services/i-file-service.js' | ||
| import type { | ||
| IMcpConfigWriter, | ||
| McpConfigExistsResult, | ||
| McpServerConfig, | ||
| } from '../../../core/interfaces/storage/i-mcp-config-writer.js' | ||
|
|
||
| import {isRecord} from '../../../utils/type-guards.js' | ||
|
|
||
| export type YamlMcpConfigWriterOptions = { | ||
| fileService: IFileService | ||
| /** | ||
| * YAML key path to the MCP server entry, including server name. | ||
| * e.g., ['mcp_servers', 'brv'] navigates to { mcp_servers: { brv: ... } } | ||
| */ | ||
| serverKeyPath: readonly string[] | ||
| } | ||
|
|
||
| function parseYamlAsRecord(content: string): Record<string, unknown> { | ||
| const parsed: unknown = yamlLoad(content) | ||
| if (!isRecord(parsed)) { | ||
| throw new TypeError('Expected YAML root to be a mapping') | ||
| } | ||
|
|
||
| return parsed | ||
| } | ||
|
|
||
| /** | ||
| * MCP config writer for YAML format files. | ||
| * Used by agents whose MCP server list lives in a YAML config (e.g. Hermes). | ||
| * Comments and key order are not preserved across round-trip — that is an | ||
| * accepted trade-off given js-yaml's capabilities. | ||
| */ | ||
| export class YamlMcpConfigWriter implements IMcpConfigWriter { | ||
| private readonly fileService: IFileService | ||
| private readonly serverKeyPath: readonly string[] | ||
|
|
||
| constructor(options: YamlMcpConfigWriterOptions) { | ||
| this.fileService = options.fileService | ||
| this.serverKeyPath = options.serverKeyPath | ||
| } | ||
|
|
||
| async exists(filePath: string): Promise<McpConfigExistsResult> { | ||
| const fileExists = await this.fileService.exists(filePath) | ||
|
|
||
| if (!fileExists) { | ||
| return {fileExists: false, serverExists: false} | ||
| } | ||
|
|
||
| try { | ||
| const content = await this.fileService.read(filePath) | ||
| const data = parseYamlAsRecord(content) | ||
| return { | ||
| fileExists: true, | ||
| serverExists: has(data, this.serverKeyPath), | ||
| } | ||
| } catch { | ||
| return {fileExists: true, serverExists: false} | ||
| } | ||
| } | ||
|
|
||
| async remove(filePath: string): Promise<boolean> { | ||
| const fileExists = await this.fileService.exists(filePath) | ||
|
|
||
| if (!fileExists) { | ||
| return false | ||
| } | ||
|
|
||
| let data: Record<string, unknown> | ||
| try { | ||
| data = parseYamlAsRecord(await this.fileService.read(filePath)) | ||
| } catch { | ||
| return false | ||
| } | ||
|
|
||
| if (!has(data, this.serverKeyPath)) { | ||
| return false | ||
| } | ||
|
|
||
| unset(data, this.serverKeyPath) | ||
| await this.fileService.write(yamlDump(data), filePath, 'overwrite') | ||
| return true | ||
| } | ||
|
|
||
| async write(filePath: string, serverConfig: McpServerConfig): Promise<void> { | ||
| let data: Record<string, unknown> = {} | ||
|
|
||
| if (await this.fileService.exists(filePath)) { | ||
| try { | ||
| data = parseYamlAsRecord(await this.fileService.read(filePath)) | ||
| } catch (error) { | ||
| const details = error instanceof Error ? error.message : String(error) | ||
| throw new Error(`Cannot update YAML MCP config at ${filePath}: ${details}`) | ||
| } | ||
| } | ||
|
|
||
| set(data, this.serverKeyPath, {...serverConfig}) | ||
| await this.fileService.write(yamlDump(data), filePath, 'overwrite') | ||
| } | ||
|
cuongdo-byterover marked this conversation as resolved.
cuongdo-byterover marked this conversation as resolved.
|
||
| } | ||
120 changes: 120 additions & 0 deletions
120
src/server/infra/connectors/shared/agent-path-resolver.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| import os from 'node:os' | ||
| import path from 'node:path' | ||
|
|
||
| /** | ||
| * Shared resolver for autonomous-agent home/config locations (Hermes, OpenClaw). | ||
| * | ||
| * Lives in `connectors/shared` so both the skill and MCP connectors can resolve | ||
| * the same root without the MCP connector importing skill internals. When no | ||
| * options are supplied the resolver falls back to `process.env` / `os.homedir()` | ||
| * so call sites without an injection seam (e.g. an MCP `configPathResolver`) | ||
| * still honor `HERMES_HOME` / `OPENCLAW_STATE_DIR` / `OPENCLAW_CONFIG_PATH`. | ||
| */ | ||
| export type AgentPathResolverOptions = { | ||
| env?: NodeJS.ProcessEnv | ||
| homeDir?: string | ||
| } | ||
|
|
||
| const resolveEnv = (options?: AgentPathResolverOptions): NodeJS.ProcessEnv => options?.env ?? process.env | ||
|
|
||
| const resolveHomeDir = (options?: AgentPathResolverOptions): string => options?.homeDir ?? os.homedir() | ||
|
|
||
| export function resolveUserPath(input: string, options?: AgentPathResolverOptions): string { | ||
| const value = input.trim() | ||
| const homeDir = resolveHomeDir(options) | ||
| if (value === '~') { | ||
| return homeDir | ||
| } | ||
|
|
||
| if (value.startsWith('~/')) { | ||
|
cuongdo-byterover marked this conversation as resolved.
|
||
| return path.join(homeDir, value.slice(2)) | ||
| } | ||
|
|
||
| if (path.isAbsolute(value)) { | ||
| return value | ||
| } | ||
|
|
||
| return path.join(homeDir, value) | ||
| } | ||
|
cuongdo-byterover marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * OpenClaw home dir, mirroring OpenClaw's `resolveRequiredHomeDir`: `OPENCLAW_HOME` | ||
| * wins (with `~` expanded against the base home), otherwise the injected/OS home. | ||
| * `options.homeDir` stands in for OpenClaw's OS-home chain (HOME/USERPROFILE/os.homedir). | ||
| */ | ||
| export function resolveOpenClawHomeDir(options?: AgentPathResolverOptions): string { | ||
| const base = resolveHomeDir(options) | ||
| const override = resolveEnv(options).OPENCLAW_HOME?.trim() | ||
| if (!override) { | ||
| return base | ||
| } | ||
|
|
||
| if (override === '~' || override.startsWith('~/') || override.startsWith('~\\')) { | ||
| return path.resolve(override.replace(/^~(?=$|[\\/])/u, base)) | ||
| } | ||
|
|
||
| return path.resolve(override) | ||
| } | ||
|
|
||
| /** | ||
| * OpenClaw path resolution, mirroring OpenClaw's `resolveHomeRelativePath`: | ||
| * `~`-prefixed expands against the OpenClaw home; every other value is | ||
| * `path.resolve`d (i.e. relative paths are CWD-relative, not home-relative). | ||
| */ | ||
| export function resolveOpenClawUserPath(input: string, options?: AgentPathResolverOptions): string { | ||
| const trimmed = input.trim() | ||
| if (!trimmed) { | ||
| return trimmed | ||
| } | ||
|
|
||
| if (trimmed.startsWith('~')) { | ||
| const home = resolveOpenClawHomeDir(options) | ||
| const expanded = trimmed === '~' ? home : path.join(home, trimmed.replace(/^~[\\/]/u, '')) | ||
| return path.resolve(expanded) | ||
| } | ||
|
|
||
| return path.resolve(trimmed) | ||
| } | ||
|
|
||
| export function resolveOpenClawStateDir(options?: AgentPathResolverOptions): string { | ||
| const override = resolveEnv(options).OPENCLAW_STATE_DIR?.trim() | ||
| if (override) { | ||
| return resolveOpenClawUserPath(override, options) | ||
| } | ||
|
|
||
| return path.join(resolveOpenClawHomeDir(options), '.openclaw') | ||
| } | ||
|
|
||
| export function resolveOpenClawConfigPath(options?: AgentPathResolverOptions): string { | ||
| const override = resolveEnv(options).OPENCLAW_CONFIG_PATH?.trim() | ||
| if (override) { | ||
| return resolveOpenClawUserPath(override, options) | ||
| } | ||
|
|
||
| return path.join(resolveOpenClawStateDir(options), 'openclaw.json') | ||
| } | ||
|
|
||
| export function resolveHermesHome(options?: AgentPathResolverOptions): string { | ||
| const override = resolveEnv(options).HERMES_HOME?.trim() | ||
| if (override) { | ||
| return resolveUserPath(override, options) | ||
| } | ||
|
|
||
| return path.join(resolveHomeDir(options), '.hermes') | ||
| } | ||
|
|
||
| /** | ||
| * Default workspace dir for the OpenClaw default agent, mirroring OpenClaw's | ||
| * `resolveDefaultAgentWorkspaceDir`. Note: this is HOME-based | ||
| * (`<home>/.openclaw/workspace`) and intentionally does NOT honor | ||
| * OPENCLAW_STATE_DIR — only the OPENCLAW_PROFILE suffix. | ||
| */ | ||
| export function resolveOpenClawDefaultWorkspaceDir(options?: AgentPathResolverOptions): string { | ||
| const profile = resolveEnv(options).OPENCLAW_PROFILE?.trim() | ||
| const home = resolveOpenClawHomeDir(options) | ||
| if (profile && profile.toLowerCase() !== 'default') { | ||
| return path.join(home, '.openclaw', `workspace-${profile}`) | ||
| } | ||
|
|
||
| return path.join(home, '.openclaw', 'workspace') | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.