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, }; }