hana-cli
Version:
HANA Developer Command Line Interface
182 lines (150 loc) • 5.33 kB
JavaScript
// @ts-check
import { buildDocEpilogue } from '../utils/doc-linker.js'
import * as baseLite from '../utils/base-lite.js'
import { fileURLToPath } from 'url'
import { dirname, join } from 'path'
import { readFileSync } from 'fs'
const __dirname = dirname(fileURLToPath(import.meta.url))
export const command = 'kb [query...]'
export const aliases = []
export const describe = baseLite.bundle.getText("kbSearch")
export const builder = (yargs) => yargs
.positional('query', {
array: true,
type: 'string',
describe: 'Search query terms'
})
.example('hana-cli kb "import csv"', 'Search for import CSV topics')
.epilog(buildDocEpilogue('kb', 'developer-tools', ['helpDocu', 'readMe']))
.example('hana-cli kb data validation', 'Search individual terms')
export async function handler(argv) {
const base = await import('../utils/base.js')
await kbSearch(argv)
}
export async function kbSearch(argv) {
const base = await import('../utils/base.js')
base.debug('kbSearch')
// Get query from positional arguments
let query = ''
if (argv.query && Array.isArray(argv.query) && argv.query.length > 0) {
query = argv.query.join(' ')
} else if (argv.query && typeof argv.query === 'string') {
query = argv.query
}
if (!query || query.trim() === '') {
console.log(baseLite.colors.yellow('Please provide a search query. Example: hana-cli kb "import csv"'))
process.exit(0)
}
try {
base.startSpinnerInt()
// Load the docs index
const docsIndexPath = join(__dirname, '../mcp-server/docs-index.json')
const docsIndexContent = readFileSync(docsIndexPath, 'utf-8')
const docsIndex = JSON.parse(docsIndexContent)
base.stopSpinnerInt()
// Search in the index
const results = performSearch(docsIndex, query, argv)
if (results.length === 0) {
console.log(baseLite.colors.yellow(`No results found for "${query}". Try different keywords.`))
process.exit(0)
}
// Display results
displayResults(results, query)
} catch (error) {
base.stopSpinnerInt()
console.error(baseLite.colors.red(`Error searching knowledge base: ${error.message}`))
}
// Explicitly exit the process
process.exit(0)
}
function performSearch(docsIndex, query, argv) {
const queryLower = query.toLowerCase()
const maxResults = argv.limit || 5
const category = argv.category ? argv.category.toLowerCase() : null
const isExact = argv.exact || false
const matchedDocs = new Map()
const byKeyword = docsIndex.index?.byKeyword || {}
const documents = docsIndex.documents || []
// Create a map of document paths to full document info
const docMap = new Map()
documents.forEach(doc => {
docMap.set(doc.path, doc)
})
if (isExact) {
// Exact phrase search - look for keywords that contain the exact phrase
Object.entries(byKeyword).forEach(([keyword, paths]) => {
if (keyword.toLowerCase().includes(queryLower)) {
paths.forEach(path => {
const doc = docMap.get(path)
if (doc) {
const score = keyword.toLowerCase() === queryLower ? 100 : 50
addResult(matchedDocs, path, doc, score)
}
})
}
})
} else {
// Keyword search - split query into tokens and search for any match
const queryTokens = queryLower.split(/\s+/).filter(t => t.length > 0)
Object.entries(byKeyword).forEach(([keyword, paths]) => {
const keywordLower = keyword.toLowerCase()
let score = 0
// Check if any token matches
for (const token of queryTokens) {
if (keywordLower.includes(token)) {
score += token.length // Longer matches score higher
}
}
if (score > 0) {
paths.forEach(path => {
const doc = docMap.get(path)
if (doc) {
addResult(matchedDocs, path, doc, score)
}
})
}
})
}
// Convert map to array and sort by score
let results = Array.from(matchedDocs.values())
.sort((a, b) => b.score - a.score)
// Filter by category if specified
if (category) {
results = results.filter(r =>
r.category?.toLowerCase() === category
)
}
// Limit results
return results.slice(0, maxResults)
}
function addResult(matchedDocs, path, doc, score) {
if (matchedDocs.has(path)) {
matchedDocs.get(path).score += score
} else {
matchedDocs.set(path, {
path: doc.path,
title: doc.title,
category: doc.category,
excerpt: doc.excerpt,
score: score
})
}
}
function displayResults(results, query) {
const colors = baseLite.colors
console.log('')
console.log(colors.cyan(`Found ${results.length} result(s) for "${query}":`))
console.log('')
results.forEach((result, index) => {
console.log(colors.bold(`${index + 1}. ${result.title}`))
console.log(colors.gray(` 📁 ${result.path}`))
console.log(colors.gray(` 📂 ${result.category}`))
if (result.excerpt) {
const truncatedExcerpt = result.excerpt.length > 200
? result.excerpt.substring(0, 200) + '...'
: result.excerpt
console.log(colors.white(` ${truncatedExcerpt}`))
}
console.log('')
})
console.log(colors.dim('💡 Open the file in your editor to read the full documentation.'))
}