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
182 lines (181 loc) • 8.24 kB
JavaScript
// 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/.
/**
* Deterministic, addressable Markdown fragments.
*
* Input is the safe, line-preserving Markdown projection, never the raw file.
* Keeping that distinction here means search can only emit selectors that
* `show` can reproduce without disclosing fenced/commented/link-target bytes.
*/
import { createHash } from "node:crypto";
import { markdownHeadingSlug, parseMarkdownToc } from "./markdown.js";
export const MARKDOWN_FRAGMENT_MAX_CHARS = 1600;
export const MARKDOWN_FRAGMENT_PREFIX = "akm-fragment-";
export const MARKDOWN_FRAGMENT_CONTEXT_DEFAULT_MAX_CHARS = 3200;
export const MARKDOWN_FRAGMENT_SELECTED_LABEL = "[Selected matching fragment]";
function hash(text) {
return createHash("sha256").update(text).digest("hex");
}
function uniqueSlugs(body) {
const out = new Map();
const seen = new Set();
for (const heading of parseMarkdownToc(body).headings) {
const base = markdownHeadingSlug(heading.text);
if (!base)
continue;
let slug = base;
for (let suffix = 1; seen.has(slug); suffix++)
slug = `${base}-${suffix}`;
seen.add(slug);
out.set(heading.line, slug);
}
return out;
}
function textOf(lines) {
return lines.join("\n").trim();
}
/** Split a large sequence only at paragraph, then word, boundaries. */
function splitPiece(piece, maxChars) {
if (textOf(piece.lines).length <= maxChars)
return [piece];
const pieces = [];
let start = 0;
while (start < piece.lines.length) {
let end = start;
let chars = 0;
while (end < piece.lines.length) {
const next = piece.lines[end];
// Let the single-line word-window path below own an oversized first
// line. Without this guard it is consumed whole before that path can
// run, silently defeating the fragment bound for transcripts/logs.
if (end === start && next.length > maxChars)
break;
if (end > start && chars + next.length + 1 > maxChars)
break;
chars += next.length + (end > start ? 1 : 0);
end++;
}
// A single long authored line needs word windows, but its source range is
// intentionally the same line for every window.
if (end === start) {
const line = piece.lines[start];
let offset = 0;
while (offset < line.length) {
let cut = Math.min(offset + maxChars, line.length);
if (cut < line.length) {
const space = line.lastIndexOf(" ", cut);
if (space > offset + Math.floor(maxChars * 0.55))
cut = space;
}
pieces.push({ lines: [line.slice(offset, cut).trim()], startLine: piece.startLine + start });
offset = cut;
while (line[offset] === " ")
offset++;
}
start++;
continue;
}
// Prefer not to split a paragraph if a blank boundary fits before `end`.
let preferred = -1;
for (let i = start + 1; i < end; i++)
if (!piece.lines[i].trim())
preferred = i;
if (preferred > start)
end = preferred;
pieces.push({ lines: piece.lines.slice(start, end), startLine: piece.startLine + start });
start = end;
while (start < piece.lines.length && !piece.lines[start].trim())
start++;
}
return pieces.filter((candidate) => textOf(candidate.lines));
}
/**
* Heading sections first, then paragraph/word windows. `startLine`/`endLine`
* always refer to the authored file's line numbers because the projection
* preserves one line per source line (with excluded bytes blanked out).
*/
export function splitMarkdownFragmentStats(body, maxChars = MARKDOWN_FRAGMENT_MAX_CHARS) {
const lines = body.split(/\r?\n/);
const headings = parseMarkdownToc(body).headings;
const boundaries = [1, ...headings.map((heading) => heading.line), lines.length + 1]
.filter((line, index, all) => index === 0 || line !== all[index - 1])
.sort((left, right) => left - right);
const slugs = uniqueSlugs(body);
const pieces = [];
let sectionCount = 0;
for (let i = 0; i < boundaries.length - 1; i++) {
const startLine = boundaries[i];
const end = boundaries[i + 1] - 1;
const section = { lines: lines.slice(startLine - 1, end), startLine, headingSlug: slugs.get(startLine) };
if (textOf(section.lines)) {
sectionCount++;
pieces.push(...splitPiece(section, maxChars));
}
}
// Whether a heading section survived as one fragment is a property of the
// complete piece set. Count once before materialization instead of scanning
// every piece for every fragment (which made many headed documents O(N²)).
const piecesPerHeading = new Map();
for (const piece of pieces) {
if (piece.headingSlug)
piecesPerHeading.set(piece.headingSlug, (piecesPerHeading.get(piece.headingSlug) ?? 0) + 1);
}
const fragments = pieces.map((piece, ordinal) => {
const text = textOf(piece.lines);
const contentLines = piece.lines.map((line, index) => ({ line, index })).filter(({ line }) => line.trim());
const first = contentLines[0]?.index ?? 0;
const last = contentLines.at(-1)?.index ?? 0;
const digest = hash(text);
const unsplitHeading = piece.headingSlug && piecesPerHeading.get(piece.headingSlug) === 1;
return {
fragmentId: `${MARKDOWN_FRAGMENT_PREFIX}${ordinal + 1}-${digest.slice(0, 12)}`,
ordinal,
startLine: piece.startLine + first,
endLine: piece.startLine + last,
...(unsplitHeading ? { headingSlug: piece.headingSlug } : {}),
text,
hash: digest,
};
});
return { fragments, hardSplitCount: Math.max(0, pieces.length - sectionCount) };
}
export function splitMarkdownFragments(body, maxChars = MARKDOWN_FRAGMENT_MAX_CHARS) {
return splitMarkdownFragmentStats(body, maxChars).fragments;
}
export function fragmentForSelector(body, selector) {
return splitMarkdownFragments(body).find((fragment) => fragment.fragmentId === selector || fragment.headingSlug === selector);
}
/**
* Assemble the document lead and selected fragment under one hard character
* budget. The selected match is always labelled and last. When both pieces do
* not fit, lead bytes are discarded before any selected-fragment bytes so the
* evidence that caused retrieval remains intact whenever the caller's budget
* can hold it.
*/
export function buildMarkdownLeadContext(fragments, selectedOrdinal, maxChars = MARKDOWN_FRAGMENT_CONTEXT_DEFAULT_MAX_CHARS) {
if (!Number.isSafeInteger(maxChars) || maxChars <= 0) {
throw new RangeError("Markdown fragment context maxChars must be a positive safe integer");
}
const selected = fragments[selectedOrdinal];
if (!selected)
throw new RangeError(`Markdown fragment ordinal ${selectedOrdinal} is out of range`);
const selectedBlock = `${MARKDOWN_FRAGMENT_SELECTED_LABEL}\n${selected.text}`;
if (selectedBlock.length > maxChars) {
return { content: selectedBlock.slice(0, maxChars), truncated: true };
}
const lead = fragments[0];
if (!lead || lead.ordinal === selected.ordinal)
return { content: selectedBlock, truncated: false };
const separator = "\n\n";
const availableLeadChars = maxChars - selectedBlock.length - separator.length;
if (availableLeadChars <= 0)
return { content: selectedBlock, truncated: true };
const leadText = lead.text.slice(0, availableLeadChars).trimEnd();
if (!leadText)
return { content: selectedBlock, truncated: true };
return {
content: `${leadText}${separator}${selectedBlock}`,
truncated: leadText.length < lead.text.length,
};
}