/** * Skill discovery — find a skill for a capability, locally or in the marketplace. * * Backs `find-skill` and `install-skill` (specs/skill-auto-install.md). It * merges three sources into one candidate list: * * 1. **Bundled** skills shipped in the npm package (`config/skills/agent/**`). * They are present on every install, so `find-skill` works for them even * when the registry has no entry or the network is down. * 2. **Installed** marketplace skills (`~/.crewly/marketplace/skills/`). * 3. **Registry** entries (public GitHub registry + crewlyai.com), fetched * with the existing marketplace service (cached, deduplicated). * * ## What "official" means * * A skill is official — and may be installed by an agent without asking — when * it is **bundled with Crewly**, or it is listed in an **official registry** * (the public GitHub registry or crewlyai.com, both curated by Crewly * maintainers) with `author` "Crewly Team" / "crewly" or `metadata.verified: * true`. Entries from the machine-local registry (`local-registry.json`, * written by local publishing) are never official, whatever author they claim. * A marketplace skill installed earlier counts as official only when the * registry still lists it as official. * * @module services/skill-setup/skill-discovery.service */ import type { MarketplaceItem, MarketplaceRegistry, InstalledItemsManifest } from '../../types/marketplace.types.js'; import { type SkillSetupManifest } from './skill-setup-manifest.js'; import { type SkillSetupRunner } from './skill-setup-runner.service.js'; /** Where a candidate was found. */ export type SkillSource = 'bundled' | 'installed' | 'registry'; /** Setup state reported for a candidate. */ export interface CandidateSetup { /** The skill declares a setup block */ declared: boolean; /** Minutes a first-time setup takes (quoted to the user) */ estimatedMinutes?: number; /** Result of a check-only probe (undefined when not probed) */ satisfied?: boolean; /** Steps that are missing (check-only probe) */ missing?: string[]; } /** One skill that could provide a capability. */ export interface SkillCandidate { /** The id `install-skill --id` takes */ id: string; name: string; description: string; tags: string[]; triggers: string[]; source: SkillSource; official: boolean; /** Why it is (or is not) official, in words */ officialReason: string; author?: string; version?: string; /** The skill's files are on this machine */ installed: boolean; /** Installed and its setup (if any) is satisfied — usable right now */ ready?: boolean; /** Local skill directory */ skillDir?: string; /** Local entry point (`/execute.sh`) */ executePath?: string; /** Registry id when it differs from `id` */ registryId?: string; setup: CandidateSetup; /** Ranking score (higher is better) */ score: number; } /** * One marketplace skill as this machine sees it — the rows of the Cloud * Portal's Marketplace → Skills tab (`GET /api/skill-setup/catalog`). */ export interface SkillCatalogEntry { /** Registry id (what `POST /skill-setup/install` and `/uninstall` take) */ id: string; name: string; description: string; tags: string[]; /** Registry category, when listed */ category?: string; author?: string; /** Registry version */ version?: string; official: boolean; officialReason: string; /** Listed by the premium (crewlyai.com) registry */ premium: boolean; /** The skill's files are on this machine */ installed: boolean; /** Where this machine's copy comes from */ source: SkillSource; /** Installed through the marketplace (in its manifest), so it can be removed; bundled skills cannot */ removable: boolean; setup: CandidateSetup; } /** A candidate plus the internals install-skill needs. */ export interface ResolvedSkill extends SkillCandidate { /** Parsed, validated setup manifest (local copy preferred, registry fallback) */ manifest?: SkillSetupManifest; /** Setup block that failed validation (message) */ manifestError?: string; /** The registry entry, when there is one */ registryItem?: MarketplaceItem; } /** Injectable dependencies. */ export interface SkillDiscoveryDeps { /** Root of bundled agent skills (default: `/config/skills/agent`) */ bundledRoot?: string; /** Directory of marketplace-installed skills */ installedRoot?: string; fetchRegistry?: () => Promise; loadManifest?: () => Promise; runner?: SkillSetupRunner; } /** * Split a query into lower-case tokens, dropping stopwords. * * @param text - Free text * @returns Tokens (CJK runs stay whole) */ export declare function tokenize(text: string): string[]; /** * Expand tokens with synonyms (QUERY_SYNONYMS). CJK tokens also match a * synonym key they contain ("语音消息" contains "语音"). * * @param tokens - Query tokens * @returns Original tokens (weight 1) and synonyms (weight 0.6), deduplicated */ export declare function expandTokens(tokens: string[]): Array<{ token: string; weight: number; }>; /** The text fields a candidate is scored on. */ export interface ScoredDoc { id: string; name: string; tags: string[]; triggers: string[]; description: string; } /** * Score how well a skill matches a query. * * Each query token (and synonym, at reduced weight) takes its best match: * exact id/name 10, exact tag 6, id/name contains 5, trigger contains 3, * tag overlap 3, description contains 1.5. The whole query appearing in a * trigger or the description adds 4. * * @param query - Free-text need ("transcribe a voice message") * @param doc - Candidate fields * @returns Score (0 = no match) */ export declare function scoreSkill(query: string, doc: ScoredDoc): number; /** * Whether a registry entry is official (see module docs). * * @param item - Registry entry (with registrySource set by fetchRegistry) * @returns `{ official, reason }` */ export declare function registryOfficial(item: MarketplaceItem): { official: boolean; reason: string; }; /** Finds and describes skills. */ export declare class SkillDiscoveryService { private readonly bundledRoot; private readonly installedRoot; private readonly fetchRegistryFn; private readonly loadManifestFn; private readonly runner; /** * @param deps - Injectable dependencies (all optional) */ constructor(deps?: SkillDiscoveryDeps); /** * `/config/skills/agent`, or null when the package root cannot be found. * * @returns Absolute path or null */ static defaultBundledRoot(): string | null; /** * Where to start looking for the package root. * * The compiled backend and CLI are ES modules, where `__dirname` does not * exist (a bare reference throws); under ts-jest it does. The entry script * (`process.argv[1]`, realpath'd through the global `bin` symlink) is * always inside the package at runtime, so it is the anchor that works * for `crewly` and the backend alike; cwd is the last resort. * * @returns Candidate start directories, most reliable first */ static packageSearchStarts(): string[]; /** * Find skills for a need, best first. * * @param query - What the agent needs ("transcribe voice message", "read pdf") * @param options - `limit` (default FIND_MAX_RESULTS); `probe` runs check-only setup on the top few (default true) * @returns Ranked candidates (score > 0) and whether the registry was reachable * * @example * ```ts * const { candidates } = await discovery.find('transcribe a voice message'); * candidates[0].id; // 'transcribe-audio' * ``` */ find(query: string, options?: { limit?: number; probe?: boolean; }): Promise<{ candidates: SkillCandidate[]; registryAvailable: boolean; }>; /** * Every marketplace skill with this machine's state: registry entries * (matched to a bundled or installed copy when there is one) plus skills * installed from a source no longer listed. Bundled skills that are not in * the registry are part of Crewly, not the marketplace, and are left out. * No setup probes run (a list must answer fast over the relay). * * @returns Entries sorted by name, and whether the registry was reachable * * @example * ```ts * const { skills } = await discovery.catalog(); * skills.filter((s) => s.installed).map((s) => s.id); * ``` */ catalog(): Promise<{ skills: SkillCatalogEntry[]; registryAvailable: boolean; }>; /** * Resolve one skill by id (local id, directory name or registry id). * * @param id - Skill id * @returns The skill, or null when neither bundled, installed nor in the registry */ resolve(id: string): Promise; /** * Resolve a skill that is on this machine (bundled or installed) without * touching the network — for `crewly skills setup` offline. * * Official status of an installed marketplace skill needs the registry, so * here it is reported as not official; bundled skills are official. * * @param id - Skill id or directory name * @returns The skill, or null when it is not on this machine */ resolveLocal(id: string): Promise; /** * Pick a candidate by id, registry id or directory name. * * @param id - Wanted id * @param all - Candidates * @returns The match or null */ private pick; /** * Run a check-only setup probe and record the result on the candidate. * * @param c - Candidate (mutated) * @returns The same candidate */ probe(c: ResolvedSkill): Promise; /** * Strip internals for API output. * * @param c - Resolved candidate * @returns Public candidate */ publicView(c: ResolvedSkill): SkillCandidate; /** * Build every candidate from all sources. * * @param options - `registry: false` skips the registry fetch * @returns Candidates and registry reachability */ private collect; /** * Candidate for a local (bundled or installed) skill. * * @param local - Local skill * @param item - Matching registry entry, if any * @returns Candidate */ private fromLocal; /** * Candidate for a registry-only skill. * * @param item - Registry entry * @param installed - The marketplace manifest lists it (files may still be missing) * @returns Candidate */ private fromRegistry; /** * Validate a raw setup block and record it on the candidate. * * @param c - Candidate (mutated) * @param raw - Raw `setup` value */ private attachSetup; /** * Scan a skills root: direct skill dirs, and one level of category dirs. * * @param root - Directory * @param source - Label for what is found * @returns Local skills */ private scanRoot; /** * Read one skill directory (SKILL.md frontmatter wins over skill.json, as in the registry builder). * * @param dir - Skill directory * @param source - Label * @returns The skill, or null when the directory has no manifest */ private readLocal; } /** * The process-wide discovery service. * * @returns The service */ export declare function getSkillDiscoveryService(): SkillDiscoveryService; //# sourceMappingURL=skill-discovery.service.d.ts.map