Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 73 additions & 32 deletions apps/desktop/src/main/services/skill-installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
}
}
Comment on lines +526 to +547
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

collectSkillDirs 会漏扫“父目录和子目录都含 SKILL.md”的场景。

Line 528 的 else 使得只要 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,
-          );
-        }
-      }
+      if (await fileExists(directMd)) {
+        result.push(baseDir);
+      }
+
+      // 也扫描一层子目录,避免漏掉“父目录与子目录都含 SKILL.md”的情况
+      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,
+        );
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/skill-installer.ts` around lines 526 - 547,
The collectSkillDirs logic currently stops scanning subdirectories when baseDir
contains SKILL.md (the else at the directMd check), missing nested skill dirs;
modify collectSkillDirs so that after detecting directMd (directMd or
baseDir/SKILL.md) it still scans the immediate children: always run the
fs.readdir + subEntries loop (or remove the else) for baseDir, checking each
sub.isDirectory() and testing path.join(nestedDir, "SKILL.md") to push nestedDir
into result; keep the existing try/catch and only skip on real read errors.


return result;
}

static async scanLocal(db: SkillDB): Promise<ScanLocalResult> {
let count = 0;
const skipped: string[] = [];
Expand All @@ -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);
Expand All @@ -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,
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
fallbackAuthor: manifest.author || "Local",
fallbackAuthor: manifest.author || undefined,
tags: parsedSkill?.frontmatter.tags,
fallbackTags: [],
instructions,
Expand All @@ -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(
Expand All @@ -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);
Expand All @@ -592,8 +639,7 @@ export class SkillInstaller {
}
}
}
}
} catch (e) {
} catch (e) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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.ts

Repository: 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.ts

Repository: 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 -n

Repository: 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 -10

Repository: 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()}")
PYEOF

Repository: 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 -20

Repository: 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"
fi

Repository: 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 -n

Repository: 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 -n

Repository: legeling/PromptHub

Length of output: 4367


try/catch 块结构存在多余闭合块导致语法错误,是阻断发布问题。

Biome 已确认在第 642 行和第 768 行处的 catch 语句报出 parse error。在这两处 catch 之前各存在 2 个多余的 } 闭合符,导致 catch 与其对应的 try 块断裂,引发解析失败。

请删除第 641 行和第 767 行各多余的一个 },使 catch 子句能正确关联其对应的 try 块。

修正方案

第一处(行 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
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/skill-installer.ts` at line 642, There are
extra closing braces before two catch blocks that break the try/catch pairing in
skill-installer.ts; remove the stray '}' immediately before each "catch (e)"
occurrence so that the catch clauses directly follow their corresponding try
blocks (specifically the two "catch (e)" blocks shown in the diff), ensuring the
try/catch structures around those sections (look for the surrounding try blocks
and the nearby functions in skill-installer.ts) are properly paired and parse
without error.

console.error(`Failed to scan path: ${scanPath}`, e);
}
}
Expand Down Expand Up @@ -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(
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -723,8 +765,7 @@ export class SkillInstaller {
}
}
}
}
} catch (e) {
} catch (e) {
console.error(`Failed to scan path: ${scanPath}`, e);
}
}),
Expand Down
25 changes: 25 additions & 0 deletions apps/desktop/src/renderer/assets/platforms/hermes.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/desktop/src/renderer/components/ui/PlatformIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import qoderIcon from "../../assets/platforms/qoder.png";
import qoderworkIcon from "../../assets/platforms/qoderwork.png";
import codebuddyLightIcon from "../../assets/platforms/codebuddy-light.svg";
import codebuddyDarkIcon from "../../assets/platforms/codebuddy-dark.svg";
import hermesIcon from "@renderer/assets/platforms/hermes.svg";

type PlatformIconSource = string | { light: string; dark: string };

Expand All @@ -55,6 +56,7 @@ const PLATFORM_ICONS: Record<string, PlatformIconSource> = {
light: codebuddyLightIcon,
dark: codebuddyDarkIcon,
},
hermes: hermesIcon,
};

// Fallback Lucide icons for platforms without PNG
Expand All @@ -76,6 +78,7 @@ const FALLBACK_ICONS: Record<string, React.ReactNode> = {
qoder: <BotIcon />,
qoderwork: <BotIcon />,
codebuddy: <BotIcon />,
hermes: <BotIcon />,
};

interface PlatformIconProps {
Expand Down
10 changes: 10 additions & 0 deletions packages/shared/constants/platforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Hardcoded hermes agent name 📘 Rule violation ⚙ Maintainability

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
## Issue description
A new platform display name is hardcoded (`Hermes Agent`) and is rendered directly in the UI.

## Issue Context
`SKILL_PLATFORMS` is used by the renderer to display `platform.name` in settings, so literals added here become user-facing UI strings.

## Fix Focus Areas
- packages/shared/constants/platforms.ts[175-184]
- apps/desktop/src/renderer/components/settings/SkillSettings.tsx[367-380]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

},
{
id: "codebuddy",
name: "CodeBuddy",
Expand Down