UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

808 lines 33.2 kB
/** * Artifact Query Engine * * Searches the artifact index by keyword, type, phase, tags, and path pattern. * Returns ranked results in human-readable or JSON format. * * @implements #416 * @source @src/artifacts/types.ts * @tests @test/unit/artifacts/query-engine.test.ts */ import { minimatch } from 'minimatch'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { loadMetadataIndex, loadGraphIndexFile } from './index-reader.js'; /** * Detect whether the workspace has project-local content (#1235). When any * of the four bundle dirs exist with at least one bundle inside, hints * pivot to the project graph instead of the framework graph. */ function detectProjectLocal(cwd) { for (const dir of ['extensions', 'addons', 'frameworks', 'plugins']) { const p = path.join(cwd, '.aiwg', dir); try { const entries = fs.readdirSync(p, { withFileTypes: true }); if (entries.some(e => e.isDirectory())) return true; } catch { // ENOENT or other — keep looking } } return false; } /** * Stop-words to drop when tokenizing a discovery phrase. Keep short — * we want the user's verbs and nouns to dominate scoring. */ const SCORE_STOPWORDS = new Set([ 'the', 'a', 'an', 'and', 'or', 'of', 'for', 'to', 'in', 'on', 'with', 'into', 'from', 'is', 'are', 'be', 'i', 'we', 'my', ]); /** * Tokenize a query phrase into lowercased keywords for multi-word * scoring. Splits on whitespace and punctuation; drops stopwords and * single-character tokens. */ function tokenize(text) { return text .toLowerCase() .split(/[^a-z0-9-]+/) .filter(t => t.length > 1 && !SCORE_STOPWORDS.has(t)); } /** * Score a metadata entry against a keyword query. * * Multi-word queries are tokenized; each token contributes its weighted * match across the entry's searchable fields. The full phrase still * earns bonus weight when it appears as a contiguous substring or as * an exact trigger phrase (the most reliable signal). * * For AIWG artifact kinds (skills/agents/commands/rules) the entry * carries `triggers` (declared activation phrases) and `capability` * (one-line description). These get the highest weights so capability * search via `aiwg index discover` ranks the right skill on top * instead of bottoming out on a path-substring match (#1214). */ /** * Normalize a string for exact-name comparison (#1233) — lowercase and * collapse `-` / `_` / whitespace runs so hyphenated kernel-skill names * like `aiwg-doctor` match queries with spaces ("aiwg doctor") and the * rendered title ("AIWG Doctor") matches the slug query. */ function normalizeName(s) { return s.toLowerCase().replace(/[-_\s]+/g, ' ').trim(); } function damerauLevenshteinAtMostOne(a, b) { if (a === b) return true; if (Math.abs(a.length - b.length) > 1) return false; if (a.length === b.length) { let firstDiff = -1; let diffCount = 0; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) { if (firstDiff < 0) firstDiff = i; diffCount++; } } if (diffCount === 1) return true; return diffCount === 2 && firstDiff + 1 < a.length && a[firstDiff] === b[firstDiff + 1] && a[firstDiff + 1] === b[firstDiff]; } const shorter = a.length < b.length ? a : b; const longer = a.length < b.length ? b : a; let i = 0; let j = 0; let edits = 0; while (i < shorter.length && j < longer.length) { if (shorter[i] === longer[j]) { i++; j++; } else { edits++; if (edits > 1) return false; j++; } } return true; } function nearNameMatch(query, name) { const queryParts = normalizeName(query).split(/\s+/).filter(Boolean); const nameParts = normalizeName(name).split(/\s+/).filter(Boolean); if (queryParts.length !== nameParts.length) return false; return queryParts.every((part, i) => { const target = nameParts[i]; if (part === target) return true; if (part.length < 5 || target.length < 5) return false; return damerauLevenshteinAtMostOne(part, target); }); } function scoreEntry(entry, text) { const lower = text.toLowerCase(); const tokens = tokenize(text); let score = 0; // Exact-name floor (#1233) — if the query (normalized) exactly matches // the entry's canonical name, this is the artifact the user is asking // for and it must surface at the top regardless of how cluttered the // rest of the corpus scoring gets. Hyphens, underscores, and whitespace // are interchangeable in the comparison so `aiwg-doctor`, `aiwg doctor`, // and `aiwg_doctor` all match a kernel skill with `name: aiwg-doctor`. // // Returns 1.001 (not 1.0) so an exact-name hit sorts above the many // capped-at-1.0 substring matches that share generic words like "use". // Display rounding (Math.round(score * 100) / 100) hides the offset, so // the user still sees `score: 1.0` while sort order is preserved. if (entry.name) { const queryNorm = normalizeName(text); const nameNorm = normalizeName(entry.name); if (queryNorm === nameNorm) { return 1.001; } if (nearNameMatch(text, entry.name)) { return 0.951; } } // Searchable text — joined once so per-token includes() is cheap const titleLower = entry.title.toLowerCase(); const summaryLower = entry.summary.toLowerCase(); const pathLower = entry.path.toLowerCase(); const typeLower = entry.type.toLowerCase(); const capabilityLower = entry.capability ? entry.capability.toLowerCase() : ''; const tagsLower = entry.tags.map(t => t.toLowerCase()); const triggersLower = entry.triggers ?? []; // For multi-token queries, require ≥50% token overlap to count // partial matches. This keeps gibberish queries (e.g., // `xyzzy_zzqwkjhg_42` after splitting on `_`) from surfacing // incidental single-token hits. const useMultiToken = tokens.length > 1; const minHits = useMultiToken ? Math.ceil(tokens.length / 2) : 1; const overlapOK = (hits) => useMultiToken && hits >= minHits; // Trigger phrase match — highest weight (4x). Exact match on the full // phrase wins big; substring or token-overlap is still strong. if (triggersLower.length > 0) { for (const trigger of triggersLower) { if (trigger === lower) { score += 0.4 * 4; break; } else if (trigger.includes(lower) || lower.includes(trigger)) { score += 0.25 * 4; } else if (useMultiToken) { const hits = tokens.filter(t => trigger.includes(t)).length; if (overlapOK(hits)) score += 0.06 * 4 * (hits / tokens.length); } } } // Capability description (2x weight) — full phrase first, then tokens if (capabilityLower) { if (capabilityLower.includes(lower)) { score += 0.2 * 2; } else if (useMultiToken) { const hits = tokens.filter(t => capabilityLower.includes(t)).length; if (overlapOK(hits)) score += 0.1 * 2 * (hits / tokens.length); } } // Title (3x weight) if (titleLower.includes(lower)) { score += 0.3 * 3; if (titleLower === lower) score += 0.2; } else if (useMultiToken) { const hits = tokens.filter(t => titleLower.includes(t)).length; if (overlapOK(hits)) score += 0.08 * 3 * (hits / tokens.length); } // Tags (2x weight) for (const tag of tagsLower) { if (tag.includes(lower)) { score += 0.2 * 2; } else if (useMultiToken) { const hits = tokens.filter(t => tag.includes(t)).length; if (overlapOK(hits)) score += 0.05 * 2 * (hits / tokens.length); } } // Summary (1x weight) if (summaryLower.includes(lower)) { score += 0.15; } else if (useMultiToken) { const hits = tokens.filter(t => summaryLower.includes(t)).length; if (overlapOK(hits)) score += 0.04 * (hits / tokens.length); } // Path (0.5x weight) if (pathLower.includes(lower)) { score += 0.1; } else if (useMultiToken) { const hits = tokens.filter(t => pathLower.includes(t)).length; if (overlapOK(hits)) score += 0.03 * (hits / tokens.length); } // Type (0.5x weight) if (typeLower.includes(lower)) { score += 0.1; } return Math.min(score, 1.0); } /** * Query the artifact index */ export async function queryIndex(cwd, params, options = {}) { const { graph } = options; const startTime = Date.now(); let candidates; if (graph) { // Single graph mode const index = loadGraphIndexFile(cwd, 'metadata.json', graph); if (!index) { console.error(`Error: No artifact index found for graph '${graph}'.`); console.log("Run 'aiwg index build' first to create the index."); process.exit(1); } candidates = Object.values(index.entries); } else { // No graph specified: search across all project-local graphs const graphTypes = ['project', 'codebase']; const allEntries = []; for (const g of graphTypes) { const idx = loadGraphIndexFile(cwd, 'metadata.json', g); if (idx) allEntries.push(...Object.values(idx.entries)); } // Fall back to legacy root index if (allEntries.length === 0) { const legacy = loadMetadataIndex(cwd); if (!legacy) { console.error('Error: No artifact index found.'); console.log("Run 'aiwg index build' first to create the index."); process.exit(1); } allEntries.push(...Object.values(legacy.entries)); } candidates = allEntries; } // Apply filters if (params.type) { candidates = candidates.filter(e => e.type === params.type); } if (params.phase) { candidates = candidates.filter(e => e.phase === params.phase); } if (params.tags && params.tags.length > 0) { candidates = candidates.filter(e => params.tags.every(tag => e.tags.includes(tag))); } if (params.path) { candidates = candidates.filter(e => minimatch(e.path, params.path)); } if (params.updatedAfter) { const cutoff = new Date(params.updatedAfter).getTime(); candidates = candidates.filter(e => new Date(e.updated).getTime() >= cutoff); } // Score and rank let results; if (params.text) { results = candidates .map(entry => ({ entry, score: scoreEntry(entry, params.text) })) .filter(r => r.score > 0) .sort((a, b) => b.score - a.score); } else { // No keyword — return all filtered results with score 1.0 results = candidates.map(entry => ({ entry, score: 1.0 })); } // Apply limit const limit = params.limit ?? 20; results = results.slice(0, limit); const queryTimeMs = Date.now() - startTime; // Output if (options.json) { console.log(JSON.stringify({ query: { text: params.text, filters: { type: params.type, phase: params.phase, tags: params.tags, path: params.path } }, results: results.map(r => ({ path: r.entry.path, type: r.entry.type, phase: r.entry.phase, title: r.entry.title, score: Math.round(r.score * 100) / 100, summary: r.entry.summary, })), total: results.length, query_time_ms: queryTimeMs, }, null, 2)); } else { const queryDesc = params.text ? `"${params.text}"` : 'all'; console.log(`Results for ${queryDesc} (${results.length} matches, ${queryTimeMs}ms):`); console.log(''); console.log(' # Score Type Phase Path'); for (let i = 0; i < results.length; i++) { const r = results[i]; const num = String(i + 1).padStart(3); const score = r.score.toFixed(2).padStart(4); const type = r.entry.type.padEnd(12).slice(0, 12); const phase = r.entry.phase.padEnd(14).slice(0, 14); console.log(` ${num} ${score} ${type} ${phase} ${r.entry.path}`); } if (results.length === 0) { console.log(' No results found.'); } console.log(''); } } const DEFAULT_DISCOVER_TYPES = ['skill', 'agent', 'command', 'rule']; /** * Resolve the AIWG installation root for path-anchoring discover output. * Discover returns paths anchored to AIWG_ROOT so the agent's `Read` * tool can resolve them from any project working directory (#1217). */ /** * Build the `run_hint` string for a script-bearing skill entry (#1227). * * Format: `aiwg run skill <name> [-- <argsHint>]`. The skill name is the * basename of its source directory (skills/<name>/SKILL.md), which is * what `aiwg run skill` accepts. */ function buildRunHint(entry) { // Use the directory basename for slug-layout skills, filename stem otherwise. const lastSlash = entry.path.lastIndexOf('/'); const tail = lastSlash >= 0 ? entry.path.slice(lastSlash + 1) : entry.path; let name; if (tail === 'SKILL.md' && lastSlash >= 0) { const dir = entry.path.slice(0, lastSlash); const dirSlash = dir.lastIndexOf('/'); name = dirSlash >= 0 ? dir.slice(dirSlash + 1) : dir; } else { name = tail.replace(/\.[^.]+$/, ''); } const argsHint = entry.script?.argsHint ? ` -- ${entry.script.argsHint}` : ''; return `aiwg run skill ${name}${argsHint}`; } async function getAiwgRootForDiscover() { if (process.env.AIWG_ROOT) return process.env.AIWG_ROOT; try { const mod = await import('../channel/manager.mjs'); if (typeof mod.getFrameworkRoot === 'function') { const r = await mod.getFrameworkRoot(); return r || null; } } catch { // channel manager unavailable — fall through } return null; } export async function discoverCapability(cwd, params) { const startTime = Date.now(); const types = params.typeFilter && params.typeFilter.length > 0 ? params.typeFilter : DEFAULT_DISCOVER_TYPES; // Default top-K = 5 (Wave A from #1218): peer-reviewed work on tool // retrieval (Semantic Tool Discovery for MCP, arXiv:2603.20313) reports // 97.1% hit@K=3 at scale; K=5 keeps a buffer above K=3 while halving // the prior K=10 default. Operators wanting more breadth can pass // `--limit N` explicitly. const limit = params.limit ?? 5; // For framework-graph entries, anchor returned paths to AIWG_ROOT so // they resolve from any project working directory (#1217). const aiwgRoot = await getAiwgRootForDiscover(); // Source: prefer `framework` graph (built post-deploy), fall back to // project / codebase / legacy depending on what's available. let entries = []; if (params.graph) { const idx = loadGraphIndexFile(cwd, 'metadata.json', params.graph); if (idx) entries = Object.values(idx.entries); } else { // Default: framework first, then any per-project graph. for (const g of ['framework', 'project', 'codebase']) { const idx = loadGraphIndexFile(cwd, 'metadata.json', g); if (idx) entries.push(...Object.values(idx.entries)); } if (entries.length === 0) { const legacy = loadMetadataIndex(cwd); if (legacy) entries.push(...Object.values(legacy.entries)); } } if (entries.length === 0) { // Empty-index case (#1221). Surface a hint that explains the gap rather // than returning a bare zero-result envelope — the latter trains agents // to conclude "AIWG doesn't have a skill for that" when in fact the // index simply hasn't been built in this workspace yet. const hint = 'No artifact index found. Run `aiwg index build --graph framework` (or `aiwg use <framework>`) first.'; if (params.json) { console.log(JSON.stringify({ query: { phrase: params.phrase, types }, results: [], total: 0, hint, }, null, 2)); } else { console.error('Error: No artifact index found.'); console.log('Run `aiwg index build --graph framework` (or `aiwg use <framework>`) first.'); } process.exit(1); } // Filter by type const candidates = entries.filter(e => types.includes(e.type)); // Score const scored = candidates .map(entry => ({ entry, score: scoreEntry(entry, params.phrase) })) .filter(r => r.score > 0) .sort((a, b) => b.score - a.score) .slice(0, limit); const queryTimeMs = Date.now() - startTime; /** * Resolve a stored framework-graph path to an absolute AIWG_ROOT * path (#1217). Framework-graph entries are stored as paths relative * to the AIWG repo root (e.g. `agentic/code/frameworks/.../SKILL.md`); * anchoring them to `AIWG_ROOT` makes them readable from any project * working directory. * * Kernel skills are anchored the same way (#1230) — `aiwg show` reads * the source corpus, not platform deploy mirrors, so kernel entries * need the same `AIWG_ROOT` anchoring as non-kernel framework entries. * Non-framework entries (project / codebase graphs) keep their stored * paths unchanged. */ function resolvePath(entry) { if (!aiwgRoot) return entry.path; if (entry.path.startsWith('/')) return entry.path; if (entry.path.startsWith('agentic/code/')) return `${aiwgRoot}/${entry.path}`; return entry.path; } // Build a hint string when the index has entries but no scored matches — // this is the second silent-failure mode #1221 calls out. When the workspace // has project-local content (.aiwg/{extensions,addons,frameworks,plugins}/) // surface a project-graph rebuild hint (#1235). const hasProjectLocal = detectProjectLocal(cwd); const emptyResultHint = scored.length === 0 ? hasProjectLocal ? `No matches in the indexed corpus. ${entries.length} entries indexed, none scored against "${params.phrase}". This workspace has project-local content under .aiwg/ — try \`aiwg index build --graph project && aiwg discover "${params.phrase}"\`, or check \`aiwg index stats --graph project\`.` : `No matches in the indexed corpus. The framework index has ${entries.length} entries but none scored against "${params.phrase}". Try a broader phrase, check \`aiwg index stats --graph framework\`, or rebuild with \`aiwg index build --graph framework --force\`.` : null; if (params.json) { console.log(JSON.stringify({ query: { phrase: params.phrase, types, limit, aiwg_root: aiwgRoot ?? null }, results: scored.map(r => ({ path: resolvePath(r.entry), type: r.entry.type, title: r.entry.title, score: Math.round(r.score * 100) / 100, triggers: r.entry.triggers ?? [], capability: r.entry.capability ?? r.entry.summary, kernel: r.entry.kernel ?? false, // #1227 — surface script-bearing skills so agents know to use // `aiwg run skill <name>` instead of executing instructions // themselves. ...(r.entry.script ? { executable: true, run_hint: buildRunHint(r.entry), } : {}), })), total: scored.length, query_time_ms: queryTimeMs, ...(emptyResultHint ? { hint: emptyResultHint } : {}), }, null, 2)); return; } if (scored.length === 0) { console.log(`No discovery matches for "${params.phrase}" in types: ${types.join(',')}.`); if (hasProjectLocal) { console.log('This workspace has project-local content under .aiwg/ —'); console.log(`try \`aiwg index build --graph project && aiwg discover "${params.phrase}"\`.`); } else { console.log('Try a broader phrase, or check `aiwg index stats --graph framework` to confirm the index is built.'); } return; } console.log(`Discovery results for "${params.phrase}" (${scored.length} matches, ${queryTimeMs}ms):`); console.log(''); for (const r of scored) { const score = r.score.toFixed(2).padStart(4); const type = r.entry.type.padEnd(7); const kernelTag = r.entry.kernel ? '★ ' : ' '; const execTag = r.entry.script ? ' [exec]' : ''; const topTrigger = r.entry.triggers && r.entry.triggers.length > 0 ? r.entry.triggers[0] : ''; console.log(` ${kernelTag}score=${score} ${type} ${resolvePath(r.entry)}${execTag}`); if (r.entry.capability) { console.log(` ${r.entry.capability}`); } if (r.entry.script) { console.log(` run: ${buildRunHint(r.entry)}`); } if (topTrigger) { console.log(` trigger: "${topTrigger}"`); } } console.log(''); console.log('★ = kernel skill (always-loaded). Others are reachable via the index.'); } /** * Scan the AIWG_ROOT corpus for an artifact matching `name` (#1221). * * Walks the well-known artifact layouts under * `agentic/code/{frameworks,addons,extensions}/<bundle>/{skills,agents,commands,rules,templates}/` * and returns the first match. Used as a fallback in `aiwg show` when an * artifact isn't in any built index — either because the workspace hasn't * been deployed to yet, or because the bundle hasn't been installed. * * Returns the resolved absolute path plus enough metadata for the caller * to surface a useful install hint. Returns null if nothing matches. */ async function findCorpusArtifact(aiwgRoot, name, typeFilter) { const fs = await import('node:fs'); const path = await import('node:path'); const fsp = fs.promises; const groups = [ { kind: 'framework', dir: path.join(aiwgRoot, 'agentic/code/frameworks') }, { kind: 'addon', dir: path.join(aiwgRoot, 'agentic/code/addons') }, { kind: 'extension', dir: path.join(aiwgRoot, 'agentic/code/extensions') }, ]; // (subdir, type, layout) — 'flat' = `<name>.md`, 'slug' = `<name>/SKILL.md` const tries = [ { sub: 'skills', type: 'skill', layout: 'slug' }, { sub: 'skills', type: 'skill', layout: 'flat' }, { sub: 'agents', type: 'agent', layout: 'flat' }, { sub: 'commands', type: 'command', layout: 'flat' }, { sub: 'rules', type: 'rule', layout: 'flat' }, { sub: 'templates', type: 'template', layout: 'flat' }, ]; for (const group of groups) { let bundles; try { bundles = (await fsp.readdir(group.dir, { withFileTypes: true })) .filter(d => d.isDirectory()) .map(d => d.name); } catch { continue; } for (const bundle of bundles) { for (const t of tries) { if (typeFilter.length > 0 && !typeFilter.includes(t.type)) continue; const candidate = t.layout === 'slug' ? path.join(group.dir, bundle, t.sub, name, 'SKILL.md') : path.join(group.dir, bundle, t.sub, `${name}.md`); try { const stat = await fsp.stat(candidate); if (stat.isFile()) { return { path: candidate, type: t.type, bundleKind: group.kind, bundleId: bundle }; } } catch { // not present — continue } } } } return null; } /** * Read and emit the full text of a specific artifact (typically a * SKILL.md). Consumers don't need to know where AIWG stores skills — * they pass the skill name and the CLI reads the file from the indexed * location. * * Lookup order: * 1. Exact path match against any indexed entry's stored path * 2. Basename match (e.g. `intake-wizard` matches an entry whose * directory basename is `intake-wizard`) * 3. Title match (case-insensitive) * 4. Corpus fallback under AIWG_ROOT (#1221) * * On ambiguity, lists all matches and exits with code 2 unless * `--first` is supplied. */ export async function showArtifact(cwd, params) { const { promises: fs } = await import('node:fs'); const path = await import('node:path'); const types = params.typeFilter && params.typeFilter.length > 0 ? params.typeFilter : DEFAULT_DISCOVER_TYPES; const aiwgRoot = await getAiwgRootForDiscover(); // Source the same graphs as discoverCapability for symmetry. let entries = []; if (params.graph) { const idx = loadGraphIndexFile(cwd, 'metadata.json', params.graph); if (idx) entries = Object.values(idx.entries); } else { for (const g of ['framework', 'project', 'codebase']) { const idx = loadGraphIndexFile(cwd, 'metadata.json', g); if (idx) entries.push(...Object.values(idx.entries)); } if (entries.length === 0) { const legacy = loadMetadataIndex(cwd); if (legacy) entries.push(...Object.values(legacy.entries)); } } if (entries.length === 0) { console.error('Error: No artifact index found.'); console.error('Run `aiwg index build --graph framework` (or `aiwg use <framework>`) first.'); process.exit(1); } const candidates = entries.filter(e => types.includes(e.type)); const needle = params.name.trim(); const needleLower = needle.toLowerCase(); // Exact path match — most precise. let matches = candidates.filter(e => e.path === needle); // Basename match — directory name (skills/<name>/SKILL.md) or filename // stem for agent/command/rule files. if (matches.length === 0) { matches = candidates.filter(e => { const dirStem = path.basename(path.dirname(e.path)); const basename = path.basename(e.path); const fileStem = path.basename(e.path).replace(/\.[^.]+$/, ''); return (basename === 'SKILL.md' && dirStem === needle) || fileStem === needle; }); } // Title match (case-insensitive) — last-resort fallback. if (matches.length === 0) { matches = candidates.filter(e => typeof e.title === 'string' && e.title.toLowerCase() === needleLower); } if (matches.length === 0) { // Corpus fallback (#1221): when the artifact isn't in any indexed graph, // scan the AIWG_ROOT corpus for a likely match and render it with an // "(uninstalled)" banner. This keeps `aiwg show` useful in workspaces // where the operator hasn't run `aiwg use` yet, or where the framework // index is stale, rather than exiting with a misleading "not found". if (aiwgRoot) { const corpusMatch = await findCorpusArtifact(aiwgRoot, needle, types); if (corpusMatch) { let content; try { content = await fs.readFile(corpusMatch.path, 'utf8'); } catch (err) { const e = err; console.error(`Error reading ${corpusMatch.path}: ${e.message ?? String(err)}`); process.exit(1); } const installHint = corpusMatch.bundleKind && corpusMatch.bundleId ? `Run \`aiwg use ${corpusMatch.bundleId}\` to install this ${corpusMatch.bundleKind} into the workspace.` : 'Run `aiwg index build --graph framework` to refresh the artifact index.'; if (params.json) { console.log(JSON.stringify({ path: corpusMatch.path, type: corpusMatch.type, title: needle, kernel: false, uninstalled: true, hint: installHint, content, }, null, 2)); return; } // Plain text mode: prepend a banner so consumers see the artifact is // corpus-only, then stream the body. const banner = [ '<!-- AIWG: corpus-only artifact — not in the indexed workspace.', ` ${installHint}`, ' Origin: ' + corpusMatch.path, '-->', '', ].join('\n'); process.stdout.write(banner); process.stdout.write(content); if (!content.endsWith('\n')) process.stdout.write('\n'); return; } } console.error(`Error: no artifact found matching "${needle}".`); console.error('Try `aiwg discover "<phrase>"` to find the right name.'); process.exit(1); } // Resolve relative framework-graph paths to absolute paths. // Kernel entries are anchored the same way as non-kernel framework entries // (#1230) — show reads the source corpus, not platform deploy mirrors. function resolvePath(entry) { if (path.isAbsolute(entry.path)) return entry.path; if (aiwgRoot && entry.path.startsWith('agentic/code/')) { return path.join(aiwgRoot, entry.path); } // Project-graph entries are stored relative to the project root (cwd). return path.join(cwd, entry.path); } if (matches.length > 1 && !params.first) { if (params.json) { console.log(JSON.stringify({ ambiguous: true, name: needle, matches: matches.map(e => ({ path: resolvePath(e), type: e.type, title: e.title, kernel: e.kernel ?? false, })), }, null, 2)); } else { console.error(`Ambiguous: "${needle}" matches ${matches.length} artifacts. Disambiguate with --type or pass the full path:`); for (const e of matches) { console.error(` ${e.type.padEnd(7)} ${resolvePath(e)}`); } console.error(''); console.error('Re-run with `--first` to pick the top match, or `--type skill` to filter.'); } process.exit(2); } const entry = matches[0]; const filePath = resolvePath(entry); let content; try { content = await fs.readFile(filePath, 'utf8'); } catch (err) { const e = err; console.error(`Error reading ${filePath}: ${e.message ?? String(err)}`); process.exit(1); } if (params.json) { console.log(JSON.stringify({ path: filePath, type: entry.type, title: entry.title, kernel: entry.kernel ?? false, // #1227 — surface script-bearing skills so callers can route to // `aiwg run skill <name>` instead of treating SKILL.md as // instructions for the agent to execute itself. ...(entry.script ? { executable: true, run_hint: buildRunHint(entry) } : {}), content, }, null, 2)); return; } // Plain mode: stream the file content unmodified so the consumer // (agent or operator) sees exactly what the source authored. // #1227 — for script-bearing skills, prepend a one-line banner so // agents reading the body immediately see the skill is executable. if (entry.script) { const hint = buildRunHint(entry); process.stdout.write(`<!-- AIWG: executable skill — run via: ${hint} -->\n`); } process.stdout.write(content); if (!content.endsWith('\n')) process.stdout.write('\n'); } //# sourceMappingURL=query-engine.js.map