Files
Hermes-ui/packages/server/src/services/hermes/skill-injector.ts
T
ekko 9a9416c99c Fix bridge history, profile models, and Windows gateway handling (#845)
* feat: support profile-aware group chat bridge flows

* feat: route cron jobs through hermes cli

* Fix group chat routing and isolate bridge tests

* Add Grok image-to-video media skill

* Default Grok videos to media directory

* Fix bridge profile fallback and cron repeat clearing

* Refine bridge chat and gateway platform handling

* Filter bridge tool-call text deltas

* Preserve structured bridge chat history

* Prepare beta release build artifacts

* Fix Windows run profile resolution

* Fix Windows path compatibility checks

* Fix profile-scoped model page display

* Hide Windows subprocess windows for jobs and updates

* Hide Windows file backend subprocess windows

* Avoid Windows gateway restart lock conflicts

* Treat Windows gateway lock as running on startup

* Force release Windows gateway lock on restart

* Tighten Windows gateway lock cleanup

* Update chat e2e source expectation

* Bump package version to 0.5.30

---------

Co-authored-by: Codex <codex@openai.com>
2026-05-19 16:09:59 +08:00

98 lines
3.2 KiB
TypeScript

import { copyFile, mkdir, readdir, rm, stat } from 'fs/promises'
import { existsSync } from 'fs'
import { join, resolve } from 'path'
import { getActiveProfileDir } from './hermes-profile'
import { logger } from '../logger'
export interface SkillInjectionResult {
sourceDir: string
targetDir: string
injected: string[]
updated: string[]
skipped: string[]
}
export class HermesSkillInjector {
constructor(
private readonly sourceDir = HermesSkillInjector.resolveSourceDir(),
private readonly targetDir = join(getActiveProfileDir(), 'skills'),
) {}
static resolveSourceDir(env: NodeJS.ProcessEnv = process.env, baseDir = __dirname): string {
const override = env.HERMES_WEB_UI_SKILLS_DIR?.trim()
if (override) return resolve(override)
const candidates = [
// Production bundle: dist/server/index.js with dist/skills copied by build.
resolve(baseDir, '../skills'),
// Development/test: packages/server/src/services/hermes -> packages/skills.
resolve(baseDir, '../../../../skills'),
// Running from repository root without bundling.
resolve(process.cwd(), 'packages/skills'),
]
return candidates.find(candidate => existsSync(candidate)) || candidates[0]
}
async injectMissingSkills(): Promise<SkillInjectionResult> {
const result: SkillInjectionResult = {
sourceDir: this.sourceDir,
targetDir: this.targetDir,
injected: [],
updated: [],
skipped: [],
}
if (!await this.isDirectory(this.sourceDir)) {
logger.debug('[skill-injector] no bundled skills directory at %s', this.sourceDir)
return result
}
await mkdir(this.targetDir, { recursive: true })
const entries = await readdir(this.sourceDir, { withFileTypes: true })
for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith('.')) continue
const sourceSkillDir = join(this.sourceDir, entry.name)
const targetSkillDir = join(this.targetDir, entry.name)
const existed = existsSync(targetSkillDir)
if (existsSync(targetSkillDir)) {
await rm(targetSkillDir, { recursive: true, force: true })
}
await this.copyDir(sourceSkillDir, targetSkillDir)
if (existed) result.updated.push(entry.name)
else result.injected.push(entry.name)
}
if (result.injected.length > 0 || result.updated.length > 0) {
logger.info({
injected: result.injected,
updated: result.updated,
targetDir: this.targetDir,
}, '[skill-injector] synced bundled skills')
}
return result
}
private async isDirectory(path: string): Promise<boolean> {
try {
return (await stat(path)).isDirectory()
} catch {
return false
}
}
private async copyDir(sourceDir: string, targetDir: string): Promise<void> {
await mkdir(targetDir, { recursive: true })
const entries = await readdir(sourceDir, { withFileTypes: true })
for (const entry of entries) {
const sourcePath = join(sourceDir, entry.name)
const targetPath = join(targetDir, entry.name)
if (entry.isDirectory()) {
await this.copyDir(sourcePath, targetPath)
} else if (entry.isFile()) {
await copyFile(sourcePath, targetPath)
}
}
}
}