From 3b756fd30d66f964741ecf3db7b901dbd34773fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 27 Mar 2026 22:38:02 -0400 Subject: [PATCH] feat: scan workspace .agents/skills/ directories for skill imports Add scanWorkspaceSkills method to skill service that iterates all workspace local paths for a company and imports SKILL.md files from .agents/skills/ subdirectories. Uses path.join for cross-platform compatibility (Windows/macOS/Linux). The scan runs automatically when skills are listed via IPC, with graceful error handling per workspace so one bad path does not block the entire listing. Also adds listWorkspaceLocalPaths to database for retrieving workspace paths by company. --- src/main/database.ts | 7 +++++++ src/main/ipc-skill-handlers.ts | 8 ++++++- src/main/skill-service.ts | 38 ++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/main/database.ts b/src/main/database.ts index fdae9572..80afd56d 100644 --- a/src/main/database.ts +++ b/src/main/database.ts @@ -1182,6 +1182,13 @@ export class AppDatabase { return row ? { ...row, isPrimary: toBoolean(row.isPrimary) } : null; } + listWorkspaceLocalPaths(companyId: string): string[] { + const rows = this.db + .prepare("select local_path as localPath from workspaces where company_id = ? and local_path is not null") + .all(companyId) as Array<{ localPath: string }>; + return rows.map((r) => r.localPath).filter(Boolean); + } + getAnyCompanyWorkspace(companyId: string): WorkspaceRecord | null { const row = this.db .prepare( diff --git a/src/main/ipc-skill-handlers.ts b/src/main/ipc-skill-handlers.ts index 5316a8a8..aadce65e 100644 --- a/src/main/ipc-skill-handlers.ts +++ b/src/main/ipc-skill-handlers.ts @@ -50,13 +50,19 @@ export function registerSkillHandlers({ return result; }); - // ---- List all skills for a company ---- + // ---- List all skills for a company (also scans workspace .agents/skills/) ---- registerHandle(IPC_CHANNELS.listSkills, async (_event, payload) => { const parsed = listSkillsSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; const serviceError = requireSkillService(); if (serviceError) return serviceError; + // Scan workspace .agents/skills/ directories for new skills before listing + try { + await skillService!.scanWorkspaceSkills(parsed.companyId); + } catch { + // Non-fatal: scan failure should not block listing existing skills + } return ok(skillService!.listForCompany(parsed.companyId)); }); diff --git a/src/main/skill-service.ts b/src/main/skill-service.ts index 0fdcdfc9..59fe57ff 100644 --- a/src/main/skill-service.ts +++ b/src/main/skill-service.ts @@ -701,6 +701,43 @@ export function createSkillService({ db, profileDir, logger }: SkillServiceDepen return inventory; } + // ---- Workspace .agents/skills/ scanning (Bug 6) ---- + + /** + * Scan all workspace local paths for a company, looking for + * `.agents/skills/` directories containing SKILL.md files. + * Auto-imports any found skills that are not already in the database. + * Cross-platform: uses path.join for correct separators. + */ + async function scanWorkspaceSkills( + companyId: string, + ): Promise { + const workspacePaths = db.listWorkspaceLocalPaths(companyId); + const allResults: SkillImportResult[] = []; + + for (const wsPath of workspacePaths) { + const agentsSkillsDir = join(wsPath, ".agents", "skills"); + if (!existsSync(agentsSkillsDir)) continue; + + try { + const stat = statSync(agentsSkillsDir); + if (!stat.isDirectory()) continue; + + const results = await importFromLocal(companyId, agentsSkillsDir); + allResults.push(...results); + logger.info( + `Scanned workspace skills: ${agentsSkillsDir} — imported ${results.length} skill(s)`, + ); + } catch (err) { + // Log but don't fail the entire scan if one workspace has issues + const message = err instanceof Error ? err.message : String(err); + logger.warn(`Failed to scan workspace skills at ${agentsSkillsDir}: ${message}`); + } + } + + return allResults; + } + // ---- Public API ---- return { @@ -715,5 +752,6 @@ export function createSkillService({ db, profileDir, logger }: SkillServiceDepen removeFromAgent, checkUpdateStatus, listRuntimeSkillEntries, + scanWorkspaceSkills, }; }