Skip to content
Open
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ skilld add crate:serde
skilld update
skilld update tailwindcss

# Build a searchable skill from the current project
skilld self
skilld search "how is authentication handled" -p self

# Search docs across installed skills
skilld search "useFetch options" -p nuxt
skilld search "error" -p nuxt --filter '{"type":"issue"}'
Expand Down Expand Up @@ -191,6 +195,7 @@ skilld config
| `skilld` | Interactive wizard (first run) or status menu (existing skills) |
| `skilld add <source...>` | Add skills. Sources: `npm:<pkg>`, `crate:<name>`, `gh:<owner/repo>`, or bare names (deprecated) |
| `skilld update [pkg]` | Update outdated skills (all or specific) |
| `skilld self` | Build a searchable skill from the current project source and docs |
| `skilld search [query]` | Search indexed docs (`-p` package, `--agents` filter, `--filter` JSON, `--limit`, `--guide`) |
| `skilld list` | List installed skills (`--json` for machine-readable output) |
| `skilld info` | Show skill info and config |
Expand Down
3 changes: 2 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function deprecatedForwarder(

// ── Subcommands (lazy-loaded) ──

const SUBCOMMAND_NAMES = ['add', 'eject', 'update', 'changes', 'watch', 'unwatch', 'info', 'list', 'config', 'remove', 'install', 'uninstall', 'search', 'cache', 'validate', 'assemble', 'setup', 'prepare', 'author', 'publish', 'upload', 'login', 'logout', 'whoami', 'pull']
const SUBCOMMAND_NAMES = ['add', 'eject', 'update', 'changes', 'watch', 'unwatch', 'info', 'list', 'config', 'remove', 'install', 'uninstall', 'search', 'self', 'cache', 'validate', 'assemble', 'setup', 'prepare', 'author', 'publish', 'upload', 'login', 'logout', 'whoami', 'pull']

// ── Main command ──

Expand All @@ -86,6 +86,7 @@ const main = defineCommand({
prepare: () => import('./commands/prepare.ts').then(m => m.prepareCommandDef),
uninstall: () => import('./commands/uninstall.ts').then(m => m.uninstallCommandDef),
search: () => import('./commands/search.ts').then(m => m.searchCommandDef),
self: () => import('./commands/self.ts').then(m => m.selfCommandDef),
cache: () => import('./commands/cache.ts').then(m => m.cacheCommandDef),
setup: () => import('./commands/wizard.ts').then(m => m.setupCommandDef),
login: () => import('./commands/login.ts').then(m => m.loginCommandDef),
Expand Down
10 changes: 6 additions & 4 deletions src/commands/search-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@ import { existsSync, readdirSync } from 'node:fs'
import * as p from '@clack/prompts'
import { join } from 'pathe'
import { getPackageDbPath, REFERENCES_DIR } from '../cache/index.ts'
import { selfIndexDbPath } from '../core/paths.ts'
import { toStoragePackageName } from '../core/prefix.ts'
import { readProjectLock } from '../core/skills.ts'

const STATIC_REGEX_1 = /[-_/]+/
const STATIC_REGEX_2 = /^(issues?|docs?|releases?):(.+)$/i

/** Collect search.db paths for packages installed in the current project (from skilld-lock.yaml) */
/** Collect project-local and installed-package search databases. */
export function findPackageDbs(packageFilter?: string, agentTypes?: AgentType[]): string[] {
const cwd = process.cwd()
const lock = readProjectLock(cwd, agentTypes)
if (!lock)
return []
return filterLockDbs(lock, packageFilter)
const packageDbs = lock ? filterLockDbs(lock, packageFilter) : []
const selfDb = selfIndexDbPath(cwd)
const includeSelf = existsSync(selfDb) && (!packageFilter || packageFilter.toLowerCase() === 'self')
return includeSelf ? [selfDb, ...packageDbs] : packageDbs
}

/** Build package name → version map from the project lockfile */
Expand Down
45 changes: 27 additions & 18 deletions src/commands/search.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { AgentType } from '../agent/index.ts'
import type { SearchFilter } from '../retriv/index.ts'
import type { SearchFilter, SearchSnippet } from '../retriv/index.ts'
import * as p from '@clack/prompts'
import { defineCommand } from 'citty'
import { detectCurrentAgent } from 'unagent/env'
Expand Down Expand Up @@ -62,18 +62,37 @@ export interface SearchCommandOptions {
limit?: number
}

export function mergeSearchResults(results: SearchSnippet[][], limit: number): SearchSnippet[] {
const seen = new Set<string>()
return results.flat()
.sort((a, b) => b.score - a.score)
.filter((result) => {
const key = `${result.package}:${result.referenceRoot ?? ''}:${result.source}:${result.lineStart}-${result.lineEnd}`
if (seen.has(key))
return false
seen.add(key)
return true
})
.slice(0, limit)
}

export async function searchCommand(rawQuery: string, opts: SearchCommandOptions = {}): Promise<void> {
const { packageFilter, limit: userLimit } = opts
const dbs = findPackageDbs(packageFilter, opts.agents)
const versions = getPackageVersions(process.cwd(), opts.agents)

if (dbs.length === 0) {
if (packageFilter) {
const available = listLockPackages(process.cwd(), opts.agents)
if (available.length > 0)
p.log.warn(`No docs indexed for "${packageFilter}". Available: ${available.join(', ')}`)
else
p.log.warn(`No docs indexed for "${packageFilter}". Run \`skilld add ${packageFilter}\` first.`)
if (packageFilter.toLowerCase() === 'self') {
p.log.warn('No project index found. Run `skilld self` first.')
}
else {
const available = listLockPackages(process.cwd(), opts.agents)
if (available.length > 0)
p.log.warn(`No docs indexed for "${packageFilter}". Available: ${available.join(', ')}`)
else
p.log.warn(`No docs indexed for "${packageFilter}". Run \`skilld add ${packageFilter}\` first.`)
}
}
else {
p.log.warn('No docs indexed yet. Run `skilld add <package>` first.')
Expand Down Expand Up @@ -103,18 +122,8 @@ export async function searchCommand(rawQuery: string, opts: SearchCommandOptions
throw err
}

// Merge, deduplicate by source+lineRange, and sort by score
const seen = new Set<string>()
const merged = allResults.flat()
.sort((a, b) => b.score - a.score)
.filter((r) => {
const key = `${r.source}:${r.lineStart}-${r.lineEnd}`
if (seen.has(key))
return false
seen.add(key)
return true
})
.slice(0, resultLimit)
// Merge, deduplicate within each package, and sort by score
const merged = mergeSearchResults(allResults, resultLimit)

const elapsed = ((performance.now() - start) / 1000).toFixed(2)

Expand Down
Loading