-
Notifications
You must be signed in to change notification settings - Fork 124
feat(platforms): add Hermes Agent platform support #110
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -498,6 +498,57 @@ export class SkillInstaller { | |
| * | ||
| * Note: This method only scans SKILL.md format skills, NOT MCP configurations. | ||
| */ | ||
| /** | ||
| * Discover skill directories under a scan path. | ||
| * Returns an array of directories that contain a SKILL.md file, | ||
| * supporting both flat and one-level nested structures. | ||
| */ | ||
| private static async collectSkillDirs( | ||
| scanPath: string, | ||
| ): Promise<string[]> { | ||
| const result: string[] = []; | ||
|
|
||
| if (!(await fileExists(scanPath))) { | ||
| return result; | ||
| } | ||
|
|
||
| const entries = await fs.readdir(scanPath, { withFileTypes: true }); | ||
| const dirsToCheck: string[] = []; | ||
|
|
||
| for (const entry of entries) { | ||
| if (entry.isDirectory()) { | ||
| dirsToCheck.push(path.join(scanPath, entry.name)); | ||
| } | ||
| } | ||
|
|
||
| for (const baseDir of dirsToCheck) { | ||
| const directMd = path.join(baseDir, "SKILL.md"); | ||
| if (await fileExists(directMd)) { | ||
| result.push(baseDir); | ||
| } else { | ||
| // Check subdirectories for category-nested structures (e.g., Hermes) | ||
| try { | ||
| const subEntries = await fs.readdir(baseDir, { withFileTypes: true }); | ||
| for (const sub of subEntries) { | ||
| if (sub.isDirectory()) { | ||
| const nestedDir = path.join(baseDir, sub.name); | ||
| if (await fileExists(path.join(nestedDir, "SKILL.md"))) { | ||
| result.push(nestedDir); | ||
| } | ||
| } | ||
| } | ||
| } catch (err) { | ||
| console.warn( | ||
| `Failed reading skill directory: ${baseDir}, skipping`, | ||
| err, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| static async scanLocal(db: SkillDB): Promise<ScanLocalResult> { | ||
| let count = 0; | ||
| const skipped: string[] = []; | ||
|
|
@@ -511,14 +562,11 @@ export class SkillInstaller { | |
|
|
||
| try { | ||
| console.log(`Scanning path for skills: ${scanPath}`); | ||
| const entries = await fs.readdir(scanPath, { withFileTypes: true }); | ||
| for (const entry of entries) { | ||
| if (entry.isDirectory()) { | ||
| const skillFolderPath = path.join(scanPath, entry.name); | ||
| const skillMdPath = path.join(skillFolderPath, "SKILL.md"); | ||
|
|
||
| if (await fileExists(skillMdPath)) { | ||
| let skillDisplayName = entry.name; | ||
| const skillDirs = await this.collectSkillDirs(scanPath); | ||
|
|
||
| for (const skillFolderPath of skillDirs) { | ||
| const skillMdPath = path.join(skillFolderPath, "SKILL.md"); | ||
| let skillDisplayName = path.basename(skillFolderPath); | ||
| try { | ||
| const instructions = await fs.readFile(skillMdPath, "utf-8"); | ||
| const manifest = await this.readManifest(skillFolderPath); | ||
|
|
@@ -529,15 +577,14 @@ export class SkillInstaller { | |
| const sanitized = sanitizeImportedSkillDraft( | ||
| { | ||
| name: parsedSkill?.frontmatter.name, | ||
| fallbackName: manifest.name || entry.name, | ||
| fallbackName: manifest.name || path.basename(skillFolderPath), | ||
| description: parsedSkill?.frontmatter.description, | ||
| fallbackDescription: | ||
| manifest.description || | ||
| `Local skill found in ${entry.name}`, | ||
| manifest.description || undefined, | ||
| version: parsedSkill?.frontmatter.version, | ||
| fallbackVersion: manifest.version, | ||
| author: parsedSkill?.frontmatter.author, | ||
|
qodo-code-review[bot] marked this conversation as resolved.
|
||
| fallbackAuthor: manifest.author || "Local", | ||
| fallbackAuthor: manifest.author || undefined, | ||
| tags: parsedSkill?.frontmatter.tags, | ||
| fallbackTags: [], | ||
| instructions, | ||
|
|
@@ -548,7 +595,7 @@ export class SkillInstaller { | |
| ); | ||
|
|
||
| const name = sanitized.name; | ||
| skillDisplayName = name || entry.name; | ||
| skillDisplayName = name || path.basename(skillFolderPath); | ||
|
|
||
| if (!name || name.trim().length === 0) { | ||
| console.warn( | ||
|
|
@@ -572,7 +619,7 @@ export class SkillInstaller { | |
| }); | ||
| count++; | ||
| console.log( | ||
| `Discovered local skill via SKILL.md: ${name} in ${entry.name}`, | ||
| `Discovered local skill via SKILL.md: ${name} in ${path.basename(skillFolderPath)}`, | ||
| ); | ||
| } catch (error: unknown) { | ||
| const msg = getErrorMessage(error); | ||
|
|
@@ -592,8 +639,7 @@ export class SkillInstaller { | |
| } | ||
| } | ||
| } | ||
| } | ||
| } catch (e) { | ||
| } catch (e) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
sed -n '620,780p' apps/desktop/src/main/services/skill-installer.ts
rg -n '^\s*}\s*catch\s*\(' apps/desktop/src/main/services/skill-installer.tsRepository: legeling/PromptHub Length of output: 7340 🏁 Script executed: # Get the exact number of lines in the file to understand its structure
wc -l apps/desktop/src/main/services/skill-installer.tsRepository: legeling/PromptHub Length of output: 116 🏁 Script executed: # Read the specific sections around lines 640-650 and 765-775 to see bracket matching
sed -n '635,650p' apps/desktop/src/main/services/skill-installer.ts | cat -n
echo "---"
sed -n '760,775p' apps/desktop/src/main/services/skill-installer.ts | cat -nRepository: legeling/PromptHub Length of output: 1100 🏁 Script executed: # Check if there's a TypeScript/JavaScript syntax validator available
# Try to find any configuration files for linters like Biome
find . -maxdepth 2 -name "biome.json" -o -name ".biomerc" -o -name "tsconfig.json" | head -10Repository: legeling/PromptHub Length of output: 75 🏁 Script executed: # Count opening and closing braces in relevant sections to verify structure
python3 << 'PYEOF'
import re
# Read the file
with open('apps/desktop/src/main/services/skill-installer.ts', 'r') as f:
lines = f.readlines()
# Extract sections around problematic lines
section1 = lines[620:650] # Around line 642
section2 = lines[755:780] # Around line 768
def analyze_braces(lines, start_line):
"""Analyze brace balance in a section"""
open_count = 0
close_count = 0
for i, line in enumerate(lines):
open_count += line.count('{')
close_count += line.count('}')
return open_count, close_count
o1, c1 = analyze_braces(section1, 620)
o2, c2 = analyze_braces(section2, 755)
print(f"Section 1 (lines 620-650): {o1} opens, {c1} closes, diff={o1-c1}")
print(f"Section 2 (lines 755-780): {o2} opens, {c2} closes, diff={o2-c2}")
# Print the actual lines to inspect
print("\n=== Section 1 (lines 620-650) ===")
for i, line in enumerate(section1, start=620):
print(f"{i}: {line.rstrip()}")
print("\n=== Section 2 (lines 755-780) ===")
for i, line in enumerate(section2, start=755):
print(f"{i}: {line.rstrip()}")
PYEOFRepository: legeling/PromptHub Length of output: 2486 🏁 Script executed: # Check if Biome is installed and can be run
which biome || npm list -g biome 2>/dev/null || echo "Biome not found globally"
cd apps/desktop && npm list biome 2>/dev/null | head -20Repository: legeling/PromptHub Length of output: 254 🏁 Script executed: # Try to run Biome on the file if available
if command -v biome &> /dev/null; then
biome check apps/desktop/src/main/services/skill-installer.ts 2>&1 | head -100
elif [ -f "apps/desktop/node_modules/.bin/biome" ]; then
apps/desktop/node_modules/.bin/biome check apps/desktop/src/main/services/skill-installer.ts 2>&1 | head -100
else
echo "Biome not available in this environment"
fiRepository: legeling/PromptHub Length of output: 2953 🏁 Script executed: # Get wider context around the first try block (likely starts earlier)
# Look backwards from line 642 to find where the try starts
sed -n '550,650p' apps/desktop/src/main/services/skill-installer.ts | cat -nRepository: legeling/PromptHub Length of output: 4681 🏁 Script executed: # Check the structure around line 768 with more context
sed -n '700,780p' apps/desktop/src/main/services/skill-installer.ts | cat -nRepository: legeling/PromptHub Length of output: 4367
Biome 已确认在第 642 行和第 768 行处的 请删除第 641 行和第 767 行各多余的一个 修正方案第一处(行 640-643): }
}
- }
- } catch (e) {
+ } catch (e) {
console.error(`Failed to scan path: ${scanPath}`, e);第二处(行 766-769): }
}
- }
- } catch (e) {
+ } catch (e) {
console.error(`Failed to scan path: ${scanPath}`, e);🧰 Tools🪛 Biome (2.4.13)[error] 642-642: Expected a catch clause but instead found '}'. (parse) [error] 642-642: Expected a statement but instead found 'catch (e)'. (parse) 🤖 Prompt for AI Agents |
||
| console.error(`Failed to scan path: ${scanPath}`, e); | ||
| } | ||
| } | ||
|
|
@@ -646,22 +692,19 @@ export class SkillInstaller { | |
| } | ||
|
|
||
| try { | ||
| const entries = await fs.readdir(scanPath, { withFileTypes: true }); | ||
| for (const entry of entries) { | ||
| if (entry.isDirectory()) { | ||
| const skillFolderPath = path.join(scanPath, entry.name); | ||
| const skillMdPath = path.join(skillFolderPath, "SKILL.md"); | ||
| const skillDirs = await SkillInstaller.collectSkillDirs(scanPath); | ||
|
|
||
| if (await fileExists(skillMdPath)) { | ||
| try { | ||
| for (const skillFolderPath of skillDirs) { | ||
| const skillMdPath = path.join(skillFolderPath, "SKILL.md"); | ||
| try { | ||
| const instructions = await fs.readFile(skillMdPath, "utf-8"); | ||
| const manifest = await this.readManifest(skillFolderPath); | ||
| const parsedSkill = parseSkillMd(instructions); | ||
|
|
||
| const name = | ||
| parsedSkill?.frontmatter.name || | ||
| manifest.name || | ||
| entry.name; | ||
| path.basename(skillFolderPath); | ||
|
|
||
| if (!name || name.trim().length === 0) { | ||
| console.warn( | ||
|
|
@@ -682,15 +725,14 @@ export class SkillInstaller { | |
| const sanitized = sanitizeImportedSkillDraft( | ||
| { | ||
| name: parsedSkill?.frontmatter.name, | ||
| fallbackName: manifest.name || entry.name, | ||
| fallbackName: manifest.name || path.basename(skillFolderPath), | ||
| description: parsedSkill?.frontmatter.description, | ||
| fallbackDescription: | ||
| manifest.description || | ||
| `Local skill found in ${entry.name}`, | ||
| manifest.description || undefined, | ||
| version: parsedSkill?.frontmatter.version, | ||
| fallbackVersion: manifest.version, | ||
| author: parsedSkill?.frontmatter.author, | ||
| fallbackAuthor: manifest.author || "Local", | ||
| fallbackAuthor: manifest.author || undefined, | ||
| tags: parsedSkill?.frontmatter.tags, | ||
| fallbackTags: [], | ||
| instructions, | ||
|
|
@@ -703,9 +745,9 @@ export class SkillInstaller { | |
| name: sanitized.name!, | ||
| description: | ||
| sanitized.description || | ||
| `Local skill found in ${entry.name}`, | ||
| manifest.description, | ||
| version: sanitized.version, | ||
| author: sanitized.author || "Local", | ||
| author: sanitized.author || manifest.author, | ||
| tags: sanitized.tags, | ||
| instructions: sanitized.instructions || instructions, | ||
| filePath: skillMdPath, | ||
|
|
@@ -723,8 +765,7 @@ export class SkillInstaller { | |
| } | ||
| } | ||
| } | ||
| } | ||
| } catch (e) { | ||
| } catch (e) { | ||
| console.error(`Failed to scan path: ${scanPath}`, e); | ||
| } | ||
| }), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -172,6 +172,16 @@ export const SKILL_PLATFORMS: SkillPlatform[] = [ | |
| linux: "~/.qoderwork/skills", | ||
| }, | ||
| }, | ||
| { | ||
| id: "hermes", | ||
| name: "Hermes Agent", | ||
| icon: "Bot", | ||
| skillsDir: { | ||
| darwin: "~/.hermes/skills", | ||
| win32: "%USERPROFILE%\\.hermes\\skills", | ||
| linux: "~/.hermes/skills", | ||
| }, | ||
|
Comment on lines
+176
to
+183
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. Hardcoded hermes agent name A new user-visible platform label Hermes Agent is hardcoded in source instead of being localized via react-i18next. This introduces non-localized UI text in the platform list/settings UI. Agent Prompt
|
||
| }, | ||
| { | ||
| id: "codebuddy", | ||
| name: "CodeBuddy", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
collectSkillDirs会漏扫“父目录和子目录都含SKILL.md”的场景。Line 528 的
else使得只要baseDir/SKILL.md存在,就不会继续扫描一层子目录;这会漏掉同目录树下的嵌套技能。🔧 建议修正
🤖 Prompt for AI Agents