UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

680 lines (679 loc) • 33 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. /** * `akm show` — entry point. * * Spec §6.2: * * show(ref) → indexer.lookup(ref) → readFile(entry.filePath) * * The richer presentation logic (matchers, renderers, edit-hints, * summary-detail truncation) lives below in this file. The flow: * * 1. Auto-index when stale so the index is current. * 2. Ask `indexer.lookup(ref)` for the row in the FTS index. * 3. Render the file via the matcher/renderer pipeline. */ import fs from "node:fs"; import path from "node:path"; import { detectAdapterId } from "../../core/adapter/detect-adapter.js"; import { recognizeMatch } from "../../core/adapter/recognize-match.js"; import { assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js"; import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js"; import { parseFrontmatter } from "../../core/asset/frontmatter.js"; import { extractSection, markdownFragmentSlugs } from "../../core/asset/markdown.js"; import { buildMarkdownLeadContext, fragmentForSelector, MARKDOWN_FRAGMENT_CONTEXT_DEFAULT_MAX_CHARS, } from "../../core/asset/markdown-fragments.js"; import { displayRef, displayRefForConceptId, typeNameFromConceptId } from "../../core/asset/resolve-ref.js"; import { META_DIR, parseMetaRef, readMetaFile } from "../../core/asset/stash-meta.js"; import { asNonEmptyString, isWithin } from "../../core/common.js"; import { loadConfig } from "../../core/config/config.js"; import { NotFoundError, rethrowIfDataDirUnreadable, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js"; import { appendEvent } from "../../core/events.js"; import { SCRIPT_EXTENSIONS } from "../../core/recognition-util.js"; import { presentationFor } from "../../core/type-presentation.js"; import { warn, warnOnce } from "../../core/warn.js"; import { lookupBundleRef, lookupBundleRefWithResolution } from "../../indexer/indexer.js"; import { projectMarkdownFragmentContent } from "../../indexer/passes/metadata.js"; import { ensurePrimaryIndexForRead, resolveReadSources } from "../../indexer/read-preflight.js"; import { buildEditHint, findSourceForPath, isEditable, resolveSourceEntries, } from "../../indexer/search/search-source.js"; import { recentShowCount, recordShowUsage } from "../../indexer/usage/show-usage.js"; import { buildFileContext, buildRenderContext, getRenderer, } from "../../indexer/walk/file-context.js"; import { resolveSourcesForOrigin } from "../../registry/origin-resolve.js"; import { withIndexDb } from "../../storage/repositories/index-db.js"; import { getIndexedMarkdownFragment } from "../../storage/repositories/index-fts-repository.js"; import { readEntryLinks } from "../../storage/repositories/index-links-repository.js"; import { getCurrentWorkflowScopeKey } from "../../workflows/authoring/scope-key.js"; import { buildWorkflowAction } from "../../workflows/renderer.js"; import { getActiveWorkflowRun } from "../../workflows/runtime/runs.js"; /** * Unified show: queries the local FTS5 index, then reads that row's file path. * A physical owner can arbitrate collisions during lookup, but show never * renders an asset without an indexed row. Spec §6.2; no provider or disk * fallback. * * When `detail` is `"brief"` or `"summary"`, the response omits * content/template/prompt and returns compact metadata. */ export async function akmShowUnified(input) { const ref = input.ref.trim(); validateFragmentContextRequest(input); // 0a. Stash `.meta/` convention: `[origin//]meta[:name]` direct-reads a // human-authored orientation doc from the stash's `.meta/` directory. // These files are not indexed (the walker skips dot-dirs), so they are // resolved here before the index lookup; meta docs are not asset refs. { const metaRef = parseMetaRef(ref); if (metaRef) return showStashMeta(metaRef); } const legacyReplacement = legacyColonRefReplacement(ref); if (legacyReplacement) { throw new NotFoundError(`The legacy colon ref "${ref}" was removed in 0.9.0. Use the slash form instead: ` + `akm show ${legacyReplacement}`); } // Env/secret bodies have no safe fragment surface, and a fragment cannot // widen what the env/secret renderers expose: both always omit the body // (env — key names only; secret — never rendered), fragment or not. Warn // and ignore the fragment rather than refusing the whole show. warnSensitiveFragmentUnsupported(parseBundleRef(ref)); // An opaque fragment selector is an indexed revision handle. Do not refresh // it away between search and show if disk changed concurrently; the stored // safe substrate below is its source of truth. Friendly heading selectors // intentionally retain the normal source-live read behavior. const parsedRef = parseBundleRef(ref); if (!parsedRef.fragment?.startsWith("akm-fragment-")) { const { primarySource } = resolveReadSources(); await ensurePrimaryIndexForRead(primarySource); } // Try local filesystem (FTS5 index lookup) const result = await showLocal(input); // Scope filter narrows resolution: if a scope filter was supplied, the // asset's frontmatter scope must satisfy every supplied key. We re-read the // file (cheap — already on the show hot path) so we don't have to thread // scope through the renderer chain just for one verification step. if (input.scope && hasAnyScopeKey(input.scope) && result.path) { enforceScopeOrThrow(result.path, ref, input.scope); } // Count prior shows of this ref before logging the current one. if (!input.skipLogging) { const consumedRef = result.ref ?? makeBundleRef(undefined, parseBundleRef(ref).conceptId); const priorShowCount = recentShowCount(consumedRef); recordShowUsage(consumedRef, result.type, result.name, input.eventSource, result.path); if (priorShowCount >= 2) { // Agent has shown this same asset 3+ times — inject a loop-break hint. result.showLoopWarning = priorShowCount + 1; } } return result; } /** Actionable migration guidance for the retired `[bundle//]type:name` spelling. */ function legacyColonRefReplacement(ref) { const match = /^(?:(?<bundle>[^/#]+)\/\/)?(?<type>[a-z][a-z0-9-]*):(?<name>[^#]+)(?<fragment>#.*)?$/i.exec(ref); const type = match?.groups?.type?.toLowerCase(); const name = match?.groups?.name; if (!type || !name) return undefined; const stashDir = stashDirFor(type); if (!stashDir) return undefined; const bundle = match?.groups?.bundle; const fragment = match?.groups?.fragment ?? ""; return `${bundle ? `${bundle}//` : ""}${stashDir}/${name}${fragment}`; } /** * Resolve a stash `.meta/` doc and return it as a lightweight ShowResponse. * * With no origin the working stash (and other configured sources, in order) * is searched and the first hit wins. With an origin the lookup is narrowed * to that stash; an uninstalled origin yields an actionable "not installed" * error. The file is read directly from disk — `.meta/` is never indexed. */ async function showStashMeta(metaRef) { const allSources = resolveSourceEntries(); const sources = resolveSourcesForOrigin(metaRef.origin, allSources); if (metaRef.origin && sources.length === 0) { throw new NotFoundError(`Stash "${metaRef.origin}" is not installed, so its ${META_DIR}/ docs are unavailable. ` + `Run: akm bundle add ${metaRef.origin}`); } const config = loadConfig(); for (const source of sources) { const metaFile = readMetaFile(source.path, metaRef.name); if (!metaFile) continue; const editable = isEditable(metaFile.path, config, allSources); appendEvent({ eventType: "show", ref: `meta:${metaRef.name}`, metadata: { type: "meta", name: metaRef.name } }); return { type: "meta", name: metaRef.name, path: metaFile.path, ref: `meta:${metaRef.name}`, content: metaFile.content, origin: source.registryId ?? null, editable, }; } throw new NotFoundError(`No ${META_DIR}/${metaRef.name} doc found${metaRef.origin ? ` in "${metaRef.origin}"` : ""}. ` + `Stash maintainers can create ${META_DIR}/${metaRef.name}.md to describe this stash ` + `(purpose, key assets, conventions, maintainer).`); } function hasAnyScopeKey(scope) { return Boolean(scope.user || scope.agent || scope.run || scope.channel); } function validateFragmentContextRequest(input) { const mode = input.contextMode ?? "exact"; if (mode !== "exact" && mode !== "lead") { throw new UsageError(`Invalid --context value: "${String(mode)}". Expected exact or lead.`, "INVALID_FLAG_VALUE"); } if (input.maxContextChars !== undefined && (!Number.isSafeInteger(input.maxContextChars) || input.maxContextChars <= 0)) { throw new UsageError("Fragment context budget must be a positive safe integer.", "INVALID_FLAG_VALUE"); } if (mode !== "lead" && input.maxContextChars !== undefined) { throw new UsageError("--max-chars and --max-tokens require --context lead.", "INVALID_FLAG_VALUE"); } if (mode !== "lead") return; if (parseMetaRef(input.ref)) { throw new UsageError("--context lead requires an indexed Markdown asset fragment.", "INVALID_FLAG_VALUE"); } const parsed = parseBundleRef(input.ref); if (!parsed.fragment) { throw new UsageError("--context lead requires a fragment-qualified ref.", "INVALID_FLAG_VALUE"); } const type = typeNameFromConceptId(parsed.conceptId)?.type; if (type === "env" || type === "secret") { throw new UsageError(`--context lead is unavailable for sensitive ${type} assets.`, "INVALID_FLAG_VALUE"); } } /** * Read the asset file's frontmatter and verify its `scope_*` keys satisfy * every supplied filter. Throws a {@link NotFoundError} on mismatch so the * caller surfaces a uniform "not found in this scope" envelope rather than * leaking out-of-scope content. */ function enforceScopeOrThrow(filePath, ref, scope) { let raw; try { raw = fs.readFileSync(filePath, "utf8"); } catch { // The file path was just resolved by the indexer/disk-walk — a read // failure here means the on-disk state moved out from under us. Treat // that as "not found in this scope" so the caller does not learn the // file's prior contents. throw new NotFoundError(`Asset not found for scope filter: ${ref}`); } const fm = parseFrontmatter(raw).data; const expected = [ ["user", scope.user], ["agent", scope.agent], ["run", scope.run], ["channel", scope.channel], ]; for (const [key, expectedValue] of expected) { if (expectedValue === undefined) continue; const actual = asNonEmptyString(fm[`scope_${key}`]); if (actual !== expectedValue) { throw new NotFoundError(`Asset "${ref}" exists but is out of scope (expected scope_${key}="${expectedValue}").`); } } } /** @internal Use akmShowUnified() for all external callers. */ export async function showLocal(input) { validateFragmentContextRequest(input); const parsed = parseBundleRef(input.ref); warnSensitiveFragmentUnsupported(parsed); const assetParts = typeNameFromConceptId(parsed.conceptId); const config = loadConfig(); const allSources = resolveSourceEntries(input.stashDir); const searchSources = resolveSourcesForOrigin(parsed.bundle, allSources); const allSourceDirs = searchSources.map((s) => s.path); const resolution = await lookupBundleRefWithResolution(parsed); if (resolution.indexError !== undefined) throw resolution.indexError; const indexedEntry = resolution.entry; const assetPath = indexedEntry?.filePath; if (!indexedEntry && parsed.bundle && searchSources.length === 0) { const installCmd = `akm bundle add ${parsed.bundle}`; throw new NotFoundError(`Stash asset not found for ref: ${makeBundleRef(parsed.bundle, parsed.conceptId)}. ` + `Stash "${parsed.bundle}" is not installed. Run: ${installCmd}`); } if (!indexedEntry && !resolution.owner) { const unrecognized = findUnrecognizedScriptSource(assetParts, searchSources); if (unrecognized) { const displayExtension = unrecognized.extension || "no extension"; warn(`Script ref "${makeBundleRef(parsed.bundle, parsed.conceptId)}" has extension "${displayExtension}", which is outside the recognized set used for indexing (${[...SCRIPT_EXTENSIONS].join(", ")}); showing it as plain text.`); const fileCtx = buildFileContext(unrecognized.sourceRoot, unrecognized.path); const renderer = await getRenderer("script-source"); if (renderer) { const match = { type: "script", specificity: 0, renderer: "script-source" }; const renderCtx = buildRenderContext(fileCtx, match, allSourceDirs); const response = renderer.buildShowResponse(renderCtx); response.name = assetParts?.name ?? response.name; return response; } } } if (!indexedEntry || !assetPath) { throw new NotFoundError(`Stash asset not found for ref: ${makeBundleRef(parsed.bundle, parsed.conceptId)}. ` + "Check the name with `akm search` or verify the asset exists in your stash."); } try { fs.accessSync(assetPath, fs.constants.R_OK); } catch (error) { throwIndexedPathNotFound(error, input.ref); } const source = findSourceForPath(assetPath, allSources); const sourceStashDir = source?.path ?? allSourceDirs[0]; if (!sourceStashDir) { throw new UsageError(`Could not determine stash root for asset: ${makeBundleRef(parsed.bundle, parsed.conceptId)}. ` + "Run `akm bundle create` to create the stash directory, or check `akm bundle list` for configured paths."); } const fileCtx = buildFileContext(sourceStashDir, assetPath); const presentedName = indexedEntry.name; const opaqueFragmentSelector = parsed.fragment?.startsWith("akm-fragment-") === true; // Friendly heading selectors retain the source-live exact response contract. // Only resolve the indexed-safe revision when the selector itself is opaque, // or when the caller explicitly opts into indexed-safe contextual output. const indexedFragment = parsed.fragment && (opaqueFragmentSelector || input.contextMode === "lead") ? withIndexDb((db) => getIndexedMarkdownFragment(db, indexedEntry.itemRef, parsed.fragment)) : undefined; const indexedFragmentContent = opaqueFragmentSelector ? indexedFragment?.content : undefined; const indexedRenderer = rendererForIndexedEntry(indexedEntry, fileCtx); let response; try { if (indexedRenderer === null) { response = buildIndexedProjectionResponse(indexedEntry, assetPath, parsed.fragment, indexedFragmentContent); } else { const match = typeof indexedRenderer === "string" ? indexedMatch(indexedEntry, indexedRenderer) : recognizeMatch(fileCtx); if (!match) { throw new UsageError(`Could not display asset "${makeBundleRef(parsed.bundle, parsed.conceptId)}" — unsupported file type or unrecognized layout`); } match.meta = { ...match.meta, name: presentedName }; const renderer = await getRenderer(match.renderer); if (!renderer) { throw new UsageError(`Renderer "${match.renderer}" not found for asset: ${makeBundleRef(parsed.bundle, parsed.conceptId)}`); } const renderBundle = indexedEntry.bundleId; const renderDefaultBundle = config.defaultBundle ?? (source?.isDefault === true ? renderBundle : undefined); const renderCtx = buildRenderContext(fileCtx, match, allSourceDirs, renderBundle, renderDefaultBundle); response = renderer.buildShowResponse(renderCtx); if (parsed.fragment !== undefined) { if (!match.renderer.endsWith("-md")) { warn(`Fragment "#${parsed.fragment}" was ignored: ${makeBundleRef(parsed.bundle, parsed.conceptId)} is not a Markdown document, so heading fragments do not apply. Showing the whole asset.`); } else { applyMarkdownFragment(response, fileCtx.content(), parsed.fragment, presentedName, indexedFragmentContent); } } } } catch (error) { throwIndexedPathNotFound(error, input.ref); } response.type = indexedEntry.type; response.name = indexedEntry.name; const isPrimaryStash = source?.isDefault === true; const displayDefaultBundle = config.defaultBundle ?? (isPrimaryStash ? indexedEntry.bundleId : undefined); const canonicalRef = displayRef({ type: indexedEntry.type, name: presentedName, conceptId: indexedEntry.conceptId, bundleId: indexedEntry.bundleId, }, displayDefaultBundle); if (parsed.fragment && indexedFragment) { const selectedFragmentId = indexedFragment.fragments[indexedFragment.ordinal].fragmentId; const selectedRef = `${canonicalRef}#${selectedFragmentId}`; const parentEstimatedTokens = typeof indexedEntry.document?.fileSize === "number" ? Math.round(indexedEntry.document.fileSize / 4) : Math.round(indexedFragment.parentChars / 4); Object.assign(response, { selectedRef, parentRef: canonicalRef, fragmentOrdinal: indexedFragment.ordinal + 1, fragmentCount: indexedFragment.count, startLine: indexedFragment.startLine, endLine: indexedFragment.endLine, ...(indexedFragment.previousFragmentId ? { previousRef: `${canonicalRef}#${indexedFragment.previousFragmentId}` } : {}), ...(indexedFragment.nextFragmentId ? { nextRef: `${canonicalRef}#${indexedFragment.nextFragmentId}` } : {}), fragmentChars: indexedFragment.fragmentChars, fragmentEstimatedTokens: Math.round(indexedFragment.fragmentChars / 4), parentChars: indexedFragment.parentChars, parentEstimatedTokens, contextMode: input.contextMode ?? "exact", contextTruncated: false, }); if (input.contextMode === "lead") { const contextMaxChars = input.maxContextChars ?? MARKDOWN_FRAGMENT_CONTEXT_DEFAULT_MAX_CHARS; const contextual = buildMarkdownLeadContext(indexedFragment.fragments, indexedFragment.ordinal, contextMaxChars); applyMarkdownResponsePayload(response, contextual.content); response.contextMaxChars = contextMaxChars; response.contextTruncated = contextual.truncated; } } else if (input.contextMode === "lead") { throw new NotFoundError(`Indexed-safe fragment context is unavailable for ${input.ref}.`); } if (response.type === "workflow") response.action = buildWorkflowAction(canonicalRef); // 07 P1-D: provenance-aware toolPolicy CEILING. An agent's self-declared // `tools` frontmatter is honoured ONLY for the operator's own PRIMARY stash — // the assets they authored. Every other source is content pulled from // elsewhere and must not name its own tool grant: registry-installed packs, a // configured secondary source, and even a git source the operator marked // `--writable` to contribute edits upstream (writability is "can I push", not // "do I trust this content to grant itself tools"). Drop the policy so dispatch // falls back to the parent/default grant. Keys off primary-stash identity — // `allSources[0]` is always the primary (search-source.ts) — not a // name-derived registryId or the orthogonal `writable` bit. `source` undefined // (unresolved path) also fails closed. if (response.toolPolicy !== undefined && !isPrimaryStash) { delete response.toolPolicy; } const editable = isEditable(assetPath, config, allSources); const fullResponse = { ...response, ref: canonicalRef, origin: source?.registryId ?? null, editable, ...(!editable ? { editHint: buildEditHint(canonicalRef) } : {}), ...showLinks(indexedEntry.itemRef, displayDefaultBundle), }; const activeRun = await getActiveWorkflowRun(getCurrentWorkflowScopeKey()); if (activeRun) { fullResponse.activeRun = activeRun; } if (input.detail === "brief") { return buildBriefResponse(fullResponse, assetPath); } if (input.detail === "summary") { return buildSummaryResponse(fullResponse, assetPath); } return fullResponse; } /** Refs listed per kind of declared link; `total` still counts them all (one memory is named by 1,437 others). */ const LINKS_PER_KIND = 10; /** * The declared links (#935) of an indexed asset, grouped by kind: outgoing in * authored order, incoming by ref, and unresolved tokens as authored. Empty * parts are omitted, and the field when nothing links either way. */ function showLinks(itemRef, defaultBundle) { let rows; try { rows = withIndexDb((db) => readEntryLinks(db, itemRef)); } catch (err) { rethrowIfTestIsolationError(err); rethrowIfDataDirUnreadable(err); return {}; } const outgoing = []; const unresolved = []; for (const row of rows.outgoing) { if (row.conceptId === undefined) unresolved.push({ kind: row.kind, ref: row.raw ?? "" }); else outgoing.push({ kind: row.kind, ref: displayRefForConceptId(row.conceptId, row.bundleId, defaultBundle) }); } const incoming = rows.incoming.map((row) => ({ kind: row.kind, ref: displayRefForConceptId(row.conceptId ?? "", row.bundleId, defaultBundle), })); const links = { ...groupLinks("outgoing", outgoing), ...groupLinks("incoming", incoming), ...groupLinks("unresolved", unresolved), }; return Object.keys(links).length > 0 ? { links } : {}; } function groupLinks(part, rows) { if (rows.length === 0) return {}; const groups = {}; for (const kind of [...new Set(rows.map((row) => row.kind))].sort()) groups[kind] = { total: 0, refs: [] }; for (const { kind, ref } of rows) { const group = groups[kind]; group.total++; if (group.refs.length < LINKS_PER_KIND) group.refs.push(ref); } return { [part]: groups }; } /** * Warn and ignore body fragments for namespaces whose authored bytes are * sensitive. `warnOnce`-keyed on the exact ref: `akmShowUnified` calls this * before delegating to `showLocal`, which calls it again as its own * defense-in-depth for callers that use `showLocal` directly — a single * request must not print the same warning twice. */ function warnSensitiveFragmentUnsupported(ref) { if (ref.fragment === undefined) return; const type = typeNameFromConceptId(ref.conceptId)?.type; if (type !== "env" && type !== "secret") return; warnOnce(`sensitive-fragment:${makeBundleRef(ref.bundle, ref.conceptId)}#${ref.fragment}`, `Fragment "#${ref.fragment}" was ignored: sensitive ${type} assets do not expose body fragments. Showing ${makeBundleRef(ref.bundle, ref.conceptId)} in full.`); } function findUnrecognizedScriptSource(assetParts, sources) { if (assetParts?.type !== "script") return undefined; const extension = path.extname(assetParts.name); if (SCRIPT_EXTENSIONS.has(extension.toLowerCase())) return undefined; const scriptDir = stashDirFor("script"); if (!scriptDir) return undefined; for (const source of sources) { try { if ((source.adapterId ?? detectAdapterId(source.path)) !== "akm") continue; const sourceRoot = path.resolve(source.path); const candidate = path.resolve(assetPathForName("script", path.join(sourceRoot, scriptDir), assetParts.name)); if (!isWithin(candidate, sourceRoot)) continue; const authoredStat = fs.lstatSync(candidate); if (!authoredStat.isFile() && !authoredStat.isSymbolicLink()) continue; const realRoot = fs.realpathSync(sourceRoot); const realCandidate = fs.realpathSync(candidate); if (!isWithin(realCandidate, realRoot) || !fs.statSync(realCandidate).isFile()) continue; return { extension, path: realCandidate, sourceRoot: realRoot }; } catch { // Missing, unreadable, dangling, or otherwise unsafe paths remain normal // not-found misses; this diagnostic never widens physical ownership. } } return undefined; } /** * Minimal `show`: ref → indexer lookup → file contents. Used by callers that * just need the raw file (e.g. clone, write-source) and don't want the full * renderer graph. Spec §6.2's literal flow. */ export async function showByRef(ref) { const parsed = parseBundleRef(ref); if (parsed.fragment !== undefined) { warn(`Fragment "#${parsed.fragment}" was ignored by raw show: ${ref}. Returning the whole file.`); } const entry = await lookupBundleRef(parsed); if (!entry) { throw new NotFoundError(`Asset not found for ref: ${makeBundleRef(parsed.bundle, parsed.conceptId)}`); } let body; try { body = await fs.promises.readFile(entry.filePath, "utf8"); } catch (error) { throwIndexedPathNotFound(error, ref); } return { filePath: entry.filePath, body }; } function throwIndexedPathNotFound(error, ref) { const code = error?.code; if (code === "ENOENT" || code === "ENOTDIR" || code === "EACCES" || code === "EPERM") { throw new NotFoundError(`The indexed file for ${ref} is missing or unreadable. The search index may be stale.`, "ASSET_NOT_FOUND", "Run `akm index` to reconcile indexed paths, then retry `akm show`."); } throw error; } /** `null` selects adapter-owned projection; a string selects a core renderer. */ function rendererForIndexedEntry(entry, _file) { if (entry.document?.ownsPresentation === true) return null; switch (entry.adapterId) { case null: case undefined: case "akm": return undefined; case "akm-workflow": return "workflow-md"; default: return presentationFor(entry.type).renderer; } } function indexedMatch(entry, renderer) { return { type: entry.type, specificity: Number.MAX_SAFE_INTEGER, renderer, meta: { name: entry.name } }; } function buildIndexedProjectionResponse(entry, assetPath, fragment, indexedFragmentContent) { const isMarkdown = path.extname(assetPath).toLowerCase() === ".md"; if (fragment !== undefined && !isMarkdown) { warn(`Fragment "#${fragment}" was ignored: ${entry.conceptId} is not a Markdown document, so heading fragments do not apply. Showing the whole asset.`); } const raw = fs.readFileSync(assetPath, "utf8"); const parsed = parseFrontmatter(raw); const content = fragment !== undefined && isMarkdown ? (indexedFragmentContent ?? requireMarkdownSection(raw, fragment, entry.name).content) : parsed.content; const description = entry.document?.description ?? asNonEmptyString(parsed.data.description); const tags = entry.document?.tags ?? (Array.isArray(parsed.data.tags) ? parsed.data.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0) : undefined); return { type: entry.type, name: entry.name, path: assetPath, action: "Read the content below.", content, ...(description ? { description } : {}), ...(tags && tags.length > 0 ? { tags } : {}), }; } function applyMarkdownFragment(response, raw, fragment, name, indexedFragmentContent) { const section = indexedFragmentContent ?? requireMarkdownSection(raw, fragment, name).content; if (response.template !== undefined) response.template = section; else if (response.prompt !== undefined) response.prompt = section; else response.content = section; } function applyMarkdownResponsePayload(response, content) { if (response.template !== undefined) response.template = content; else if (response.prompt !== undefined) response.prompt = content; else response.content = content; } function requireMarkdownSection(content, fragment, name) { const section = extractSection(content, fragment); if (section) return section; const indexed = projectMarkdownFragmentContent(content); const safeFragment = indexed ? fragmentForSelector(indexed, fragment) : undefined; if (safeFragment) { return { content: safeFragment.text, startLine: safeFragment.startLine, endLine: safeFragment.endLine }; } const available = markdownFragmentSlugs(content); throw new NotFoundError(`Fragment "#${fragment}" not found in ${name}.` + (available.length > 0 ? ` Available fragments: ${available.map((slug) => `#${slug}`).join(", ")}.` : "")); } /** * Build a reduced brief response from a full ShowResponse. * * Keeps routing/identification fields while omitting content/template/prompt. */ function buildBriefResponse(full, assetPath) { const summary = buildSummaryResponse(full, assetPath); return { type: summary.type, name: summary.name, path: summary.path, ...(summary.ref ? { ref: summary.ref } : {}), ...(summary.description ? { description: summary.description } : {}), ...(summary.action ? { action: summary.action } : {}), ...(summary.run ? { run: summary.run } : {}), ...(summary.origin !== undefined ? { origin: summary.origin } : {}), ...(full.editable !== undefined ? { editable: full.editable } : {}), ...(full.editHint ? { editHint: full.editHint } : {}), ...fragmentResponseProjection(full), }; } /** * Build a compact summary response from a full ShowResponse. * * Strips content/template/prompt and returns only metadata fields: * type, name, path, description, tags, parameters, action. * Enriches description and tags from rendered content when available. * * The resulting JSON should be under 200 tokens. */ function buildSummaryResponse(full, assetPath) { let description = full.description; const tags = full.tags; if (assetPath) { const textContent = full.content ?? full.template ?? full.prompt; if (textContent && !description) { const parsed = parseFrontmatter(textContent); description = asNonEmptyString(parsed.data.description); } } const summary = { type: full.type, name: full.name, path: full.path, ...(full.ref ? { ref: full.ref } : {}), ...(description ? { description } : {}), ...(tags && tags.length > 0 ? { tags } : {}), ...(full.parameters ? { parameters: full.parameters } : {}), ...(full.workflowTitle ? { workflowTitle: full.workflowTitle } : {}), ...(full.action ? { action: full.action } : {}), ...(full.run ? { run: full.run } : {}), ...(full.origin !== undefined ? { origin: full.origin } : {}), ...(full.editable !== undefined ? { editable: full.editable } : {}), ...(full.editHint ? { editHint: full.editHint } : {}), ...fragmentResponseProjection(full), }; return summary; } function fragmentResponseProjection(full) { if (!full.selectedRef) return {}; return { selectedRef: full.selectedRef, parentRef: full.parentRef, fragmentOrdinal: full.fragmentOrdinal, fragmentCount: full.fragmentCount, startLine: full.startLine, endLine: full.endLine, previousRef: full.previousRef, nextRef: full.nextRef, fragmentChars: full.fragmentChars, fragmentEstimatedTokens: full.fragmentEstimatedTokens, parentChars: full.parentChars, parentEstimatedTokens: full.parentEstimatedTokens, contextMode: full.contextMode, contextMaxChars: full.contextMaxChars, contextTruncated: full.contextTruncated, }; }