oscar64-mcp-docs 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/stdio.js +147 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ The executable entry is built to `dist/stdio.js`.
|
|
|
76
76
|
|
|
77
77
|
Primary tools:
|
|
78
78
|
|
|
79
|
-
- `search(query, limit, system, include_details)` -> unified manual/code search with strict hit fields (`source`, `uri`, `title`, `
|
|
79
|
+
- `search(query, limit, type, system, include_details)` -> unified manual/code search with strict hit fields (`source`, `result_type`, `uri`, `title`, `preview`, `score`, `classification_summary`), optional `referenced_files` as `code://...` URIs readable by `read_uri`, and optional `classification_details`; `type` defaults to `all`; `system` defaults to `c64` and supports `all` for cross-system results
|
|
80
80
|
- `read_uri(uri, binary_mode, max_base64_bytes)` -> returns `ok + data` where `data.content_type` is `text` or `binary` for `docs://...` and `code://...`
|
|
81
81
|
- `list_indexes(type, system)` -> lists `topics`/`tutorials`/`samples`/`headers` entries; `type` defaults to `headers`, `system` defaults to `c64`, and `system=all` returns cross-system indexes
|
|
82
82
|
|
package/dist/stdio.js
CHANGED
|
@@ -296,6 +296,7 @@ function matchesSystemFilter(systems, filter) {
|
|
|
296
296
|
import { z } from "zod";
|
|
297
297
|
var systemFamilySchema = z.enum(["c64", "c128", "vic20", "plus4", "nes", "x16", "pet", "atari", "mega65"]);
|
|
298
298
|
var systemFilterSchema = z.enum(["all", "c64", "c128", "vic20", "plus4", "nes", "x16", "pet", "atari", "mega65"]);
|
|
299
|
+
var searchTypeSchema = z.enum(["all", "topics", "tutorials", "samples", "headers"]);
|
|
299
300
|
var toolErrorCodeSchema = z.enum([
|
|
300
301
|
"INVALID_INPUT",
|
|
301
302
|
"NOT_FOUND",
|
|
@@ -343,11 +344,18 @@ var classificationSummarySchema = z.object({
|
|
|
343
344
|
systems: z.array(systemFamilySchema).describe("Detected target systems for this result; empty means shared/common."),
|
|
344
345
|
scope: z.enum(["tutorial", "sample", "manual"]).describe("Content scope of this result.")
|
|
345
346
|
});
|
|
347
|
+
var searchPreviewSchema = z.object({
|
|
348
|
+
summary: z.string().describe("Compact preview summary for quick relevance checks."),
|
|
349
|
+
signature: z.string().optional().describe("Declaration-like line extracted from content when available."),
|
|
350
|
+
include_path: z.string().optional().describe("Header include path context when relevant."),
|
|
351
|
+
declaration_context: z.string().optional().describe("Nearby declaration context that helps disambiguate similar APIs.")
|
|
352
|
+
});
|
|
346
353
|
var searchHitSchema = z.object({
|
|
347
354
|
source: z.enum(["manual", "code"]).describe("Where the result came from."),
|
|
355
|
+
result_type: z.enum(["topics", "tutorials", "samples", "headers"]).describe("Artifact type for this result."),
|
|
348
356
|
uri: z.string().describe("URI to pass into `read_uri` for full content."),
|
|
349
357
|
title: z.string().describe("Short title for the matched result."),
|
|
350
|
-
|
|
358
|
+
preview: searchPreviewSchema.describe("Structured preview fields for relevance evaluation."),
|
|
351
359
|
score: z.number().describe("Relative relevance score; higher means better match."),
|
|
352
360
|
referenced_files: z.array(z.string()).optional().describe("Referenced `code://...` URIs that can be read with `read_uri` (for example from #embed or #include)."),
|
|
353
361
|
classification_summary: classificationSummarySchema.describe("Compact classification metadata always returned."),
|
|
@@ -356,6 +364,7 @@ var searchHitSchema = z.object({
|
|
|
356
364
|
var searchInputSchema = z.object({
|
|
357
365
|
query: z.string().min(1).describe("Query text, symbol, API name, or error phrase to search for."),
|
|
358
366
|
limit: z.number().int().min(1).max(80).default(20).describe("Maximum number of results to return."),
|
|
367
|
+
type: searchTypeSchema.default("all").describe("Filter results by artifact type. Defaults to `all`."),
|
|
359
368
|
system: systemFilterSchema.default("c64").describe("Target system filter. Defaults to `c64`; use `all` for cross-system search."),
|
|
360
369
|
include_details: z.boolean().default(false).describe("Set to true to include full classification details and evidence per hit.")
|
|
361
370
|
});
|
|
@@ -763,6 +772,29 @@ function inferCombineMode(query) {
|
|
|
763
772
|
function codeUri(scope, relPath) {
|
|
764
773
|
return `code://${scope}/${relPath.replace(/\\/g, "/")}`;
|
|
765
774
|
}
|
|
775
|
+
function extractDeclarationInfo(text) {
|
|
776
|
+
const lines = text.split(/\r?\n/);
|
|
777
|
+
const looksLikeDeclaration = (line) => {
|
|
778
|
+
const trimmed = line.trim();
|
|
779
|
+
if (!trimmed) return false;
|
|
780
|
+
if (/^#\s*define\s+[A-Za-z_][A-Za-z0-9_]*/.test(trimmed)) return true;
|
|
781
|
+
if (/^(?:extern|typedef)\b.+;/.test(trimmed)) return true;
|
|
782
|
+
if (/^[A-Za-z_][A-Za-z0-9_\s\*]+\([^;{}]*\)\s*;/.test(trimmed)) return true;
|
|
783
|
+
return false;
|
|
784
|
+
};
|
|
785
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
786
|
+
const line = lines[i] ?? "";
|
|
787
|
+
if (!looksLikeDeclaration(line)) continue;
|
|
788
|
+
const prev = lines.slice(Math.max(0, i - 1), i).find((v) => v.trim().length > 0)?.trim();
|
|
789
|
+
const next = lines.slice(i + 1, i + 3).find((v) => v.trim().length > 0)?.trim();
|
|
790
|
+
const context = [prev, line.trim(), next].filter(Boolean).join(" | ");
|
|
791
|
+
return {
|
|
792
|
+
signature: line.trim(),
|
|
793
|
+
declarationContext: context || void 0
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
return {};
|
|
797
|
+
}
|
|
766
798
|
function extractReferencedFiles(text) {
|
|
767
799
|
const refs = /* @__PURE__ */ new Set();
|
|
768
800
|
const addMatches = (regex, group = 1) => {
|
|
@@ -790,7 +822,7 @@ function resolveCollectionClassification(entries, relPath) {
|
|
|
790
822
|
async function buildSearchIndex(state) {
|
|
791
823
|
const ms = new MiniSearch({
|
|
792
824
|
fields: ["title", "body"],
|
|
793
|
-
storeFields: ["source", "uri", "title", "
|
|
825
|
+
storeFields: ["source", "resultType", "uri", "title", "preview", "body", "classification", "referencedFiles"],
|
|
794
826
|
tokenize: oscarTokenizer,
|
|
795
827
|
processTerm: oscarProcessTerm,
|
|
796
828
|
searchOptions: {
|
|
@@ -806,9 +838,12 @@ async function buildSearchIndex(state) {
|
|
|
806
838
|
docs.push({
|
|
807
839
|
id: `manual:${section.anchor}`,
|
|
808
840
|
source: "manual",
|
|
841
|
+
resultType: "topics",
|
|
809
842
|
uri: `docs://oscar64/manual#${section.anchor}`,
|
|
810
843
|
title: section.heading,
|
|
811
|
-
|
|
844
|
+
preview: {
|
|
845
|
+
summary: makeExcerpt(body)
|
|
846
|
+
},
|
|
812
847
|
body,
|
|
813
848
|
classification: classifyV2({
|
|
814
849
|
scope: "manual",
|
|
@@ -820,10 +855,16 @@ async function buildSearchIndex(state) {
|
|
|
820
855
|
}
|
|
821
856
|
const roots = [
|
|
822
857
|
{ scope: "tutorial", absRoot: state.tutorialsRoot },
|
|
823
|
-
{ scope: "sample", absRoot: state.samplesRoot, uriPrefix: "samples" }
|
|
858
|
+
{ scope: "sample", absRoot: state.samplesRoot, uriPrefix: "samples" },
|
|
859
|
+
{ scope: "oscar", absRoot: path3.join(state.oscar64Root, "include"), uriPrefix: "include" }
|
|
824
860
|
];
|
|
825
861
|
for (const root of roots) {
|
|
826
|
-
|
|
862
|
+
let files = [];
|
|
863
|
+
try {
|
|
864
|
+
files = await listFilesRecursive(root.absRoot);
|
|
865
|
+
} catch {
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
827
868
|
for (const abs of files) {
|
|
828
869
|
const rel = abs.replace(root.absRoot + "/", "").replace(/\\/g, "/");
|
|
829
870
|
const uriRel = root.uriPrefix ? `${root.uriPrefix}/${rel}` : rel;
|
|
@@ -834,18 +875,26 @@ async function buildSearchIndex(state) {
|
|
|
834
875
|
} catch {
|
|
835
876
|
continue;
|
|
836
877
|
}
|
|
837
|
-
const collectionClassification = root.scope === "tutorial" ? resolveCollectionClassification(state.tutorials, rel) : resolveCollectionClassification(state.samples, rel);
|
|
878
|
+
const collectionClassification = root.scope === "tutorial" ? resolveCollectionClassification(state.tutorials, rel) : root.scope === "sample" ? resolveCollectionClassification(state.samples, rel) : null;
|
|
879
|
+
const declarationInfo = extractDeclarationInfo(text);
|
|
880
|
+
const resultType = root.scope === "tutorial" ? "tutorials" : root.scope === "sample" ? "samples" : "headers";
|
|
838
881
|
docs.push({
|
|
839
882
|
id: `${root.scope}:${uriRel}`,
|
|
840
883
|
source: "code",
|
|
841
|
-
|
|
884
|
+
resultType,
|
|
885
|
+
uri: root.scope === "sample" && !uriRel.startsWith("samples/") ? codeUri("oscar", uriRel) : codeUri(root.scope === "oscar" ? "oscar" : root.scope, uriRel),
|
|
842
886
|
title: path3.basename(uriRel),
|
|
843
|
-
|
|
887
|
+
preview: {
|
|
888
|
+
summary: makeExcerpt(text, 700),
|
|
889
|
+
...declarationInfo.signature ? { signature: declarationInfo.signature } : {},
|
|
890
|
+
...declarationInfo.declarationContext ? { declarationContext: declarationInfo.declarationContext } : {},
|
|
891
|
+
...resultType === "headers" ? { includePath: uriRel.replace(/^include\//, "") } : {}
|
|
892
|
+
},
|
|
844
893
|
body: `${uriRel}
|
|
845
894
|
${text}`,
|
|
846
895
|
referencedFiles: extractReferencedFiles(text),
|
|
847
896
|
classification: collectionClassification ?? classifyV2({
|
|
848
|
-
scope: root.scope,
|
|
897
|
+
scope: root.scope === "oscar" ? "sample" : root.scope,
|
|
849
898
|
title: path3.basename(uriRel),
|
|
850
899
|
relPath: uriRel,
|
|
851
900
|
text
|
|
@@ -1605,8 +1654,90 @@ async function resolveReferencedUris(state, sourceUri, refs) {
|
|
|
1605
1654
|
}
|
|
1606
1655
|
return out.length > 0 ? out : void 0;
|
|
1607
1656
|
}
|
|
1657
|
+
function escapeRegExp(value) {
|
|
1658
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1659
|
+
}
|
|
1660
|
+
function looksLikeDeclarationLine(line) {
|
|
1661
|
+
const trimmed = line.trim();
|
|
1662
|
+
if (!trimmed) return false;
|
|
1663
|
+
if (/^#\s*define\s+[A-Za-z_][A-Za-z0-9_]*/.test(trimmed)) return true;
|
|
1664
|
+
if (/^(?:extern|typedef)\b.+;/.test(trimmed)) return true;
|
|
1665
|
+
if (/^[A-Za-z_][A-Za-z0-9_\s\*]+\([^;{}]*\)\s*;/.test(trimmed)) return true;
|
|
1666
|
+
return false;
|
|
1667
|
+
}
|
|
1668
|
+
function inferResultType(result) {
|
|
1669
|
+
if (result?.resultType === "topics" || result?.resultType === "tutorials" || result?.resultType === "samples" || result?.resultType === "headers") {
|
|
1670
|
+
return result.resultType;
|
|
1671
|
+
}
|
|
1672
|
+
const uri = String(result?.uri ?? "");
|
|
1673
|
+
if (uri.startsWith("docs://")) return "topics";
|
|
1674
|
+
if (uri.startsWith("code://tutorial/")) return "tutorials";
|
|
1675
|
+
if (uri.startsWith("code://sample/")) return "samples";
|
|
1676
|
+
if (uri.startsWith("code://oscar/include/") && uri.toLowerCase().endsWith(".h")) return "headers";
|
|
1677
|
+
return "samples";
|
|
1678
|
+
}
|
|
1679
|
+
function buildPreview(result, query) {
|
|
1680
|
+
const uri = String(result?.uri ?? "");
|
|
1681
|
+
const body = String(result?.body ?? "");
|
|
1682
|
+
const fallbackSummary = makeExcerpt(String(result?.snippet ?? ""), 700);
|
|
1683
|
+
const basePreview = typeof result?.preview === "object" && result.preview ? result.preview : { summary: fallbackSummary };
|
|
1684
|
+
let signature = typeof basePreview.signature === "string" && basePreview.signature.trim().length > 0 ? basePreview.signature.trim() : void 0;
|
|
1685
|
+
let declarationContext = typeof basePreview.declarationContext === "string" && basePreview.declarationContext.trim().length > 0 ? basePreview.declarationContext.trim() : void 0;
|
|
1686
|
+
if (body && query.trim()) {
|
|
1687
|
+
const queryWord = query.trim().toLowerCase();
|
|
1688
|
+
const matcher = new RegExp(`\\b${escapeRegExp(queryWord)}\\b`);
|
|
1689
|
+
const lines = body.split(/\r?\n/);
|
|
1690
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1691
|
+
const line = String(lines[i] ?? "");
|
|
1692
|
+
const lower = line.toLowerCase();
|
|
1693
|
+
if (!lower.includes(queryWord)) continue;
|
|
1694
|
+
if (!matcher.test(lower) || !looksLikeDeclarationLine(line)) continue;
|
|
1695
|
+
signature = signature ?? line.trim();
|
|
1696
|
+
const prev = lines.slice(Math.max(0, i - 1), i).find((v) => String(v).trim().length > 0)?.trim();
|
|
1697
|
+
const next = lines.slice(i + 1, i + 3).find((v) => String(v).trim().length > 0)?.trim();
|
|
1698
|
+
declarationContext = declarationContext ?? [prev, line.trim(), next].filter(Boolean).join(" | ");
|
|
1699
|
+
break;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
const includePathFromUri = uri.startsWith("code://oscar/include/") ? uri.replace("code://oscar/include/", "") : void 0;
|
|
1703
|
+
const includePath = typeof basePreview.includePath === "string" && basePreview.includePath.trim().length > 0 ? basePreview.includePath.trim() : includePathFromUri;
|
|
1704
|
+
const summaryCandidate = typeof basePreview.summary === "string" && basePreview.summary.trim().length > 0 ? basePreview.summary : fallbackSummary;
|
|
1705
|
+
return {
|
|
1706
|
+
summary: summaryCandidate || makeExcerpt(body || String(result?.title ?? ""), 700),
|
|
1707
|
+
...signature ? { signature } : {},
|
|
1708
|
+
...includePath ? { include_path: includePath } : {},
|
|
1709
|
+
...declarationContext ? { declaration_context: declarationContext } : {}
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
function computeSymbolBoost(result, query) {
|
|
1713
|
+
const symbol = query.trim();
|
|
1714
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(symbol)) return 0;
|
|
1715
|
+
const symbolLower = symbol.toLowerCase();
|
|
1716
|
+
let boost = 0;
|
|
1717
|
+
const title = String(result?.title ?? "").toLowerCase();
|
|
1718
|
+
const titleBase = title.replace(/\.[a-z0-9]+$/i, "");
|
|
1719
|
+
if (title === symbolLower || titleBase === symbolLower) boost += 90;
|
|
1720
|
+
const body = String(result?.body ?? "");
|
|
1721
|
+
if (body) {
|
|
1722
|
+
const exactWord = new RegExp(`\\b${escapeRegExp(symbolLower)}\\b`);
|
|
1723
|
+
const lines = body.split(/\r?\n/);
|
|
1724
|
+
for (const line of lines) {
|
|
1725
|
+
const lower = line.toLowerCase();
|
|
1726
|
+
if (!lower.includes(symbolLower)) continue;
|
|
1727
|
+
if (exactWord.test(lower)) {
|
|
1728
|
+
boost = Math.max(boost, looksLikeDeclarationLine(line) ? 70 : 28);
|
|
1729
|
+
break;
|
|
1730
|
+
}
|
|
1731
|
+
boost = Math.max(boost, 10);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
const uri = String(result?.uri ?? "");
|
|
1735
|
+
if (boost > 0 && uri.startsWith("code://oscar/include/")) boost += 12;
|
|
1736
|
+
return boost;
|
|
1737
|
+
}
|
|
1608
1738
|
async function executeSearch(context) {
|
|
1609
1739
|
const { query, limit, include_details } = context;
|
|
1740
|
+
const requestedType = context.type ?? "all";
|
|
1610
1741
|
const system = context.system ?? "c64";
|
|
1611
1742
|
const state = await getStateSnapshot();
|
|
1612
1743
|
const toFallbackClassification = () => ({
|
|
@@ -1672,14 +1803,17 @@ async function executeSearch(context) {
|
|
|
1672
1803
|
rawResults.map(async (result) => {
|
|
1673
1804
|
const details = toDetails(result.classification);
|
|
1674
1805
|
const referencedUris = await resolveReferencedUris(state, String(result.uri ?? ""), result.referencedFiles);
|
|
1806
|
+
const resultType = inferResultType(result);
|
|
1807
|
+
const rerankBoost = computeSymbolBoost(result, query);
|
|
1675
1808
|
return {
|
|
1676
|
-
score: result.score ?? 0,
|
|
1809
|
+
score: (result.score ?? 0) + rerankBoost,
|
|
1677
1810
|
hit: {
|
|
1678
1811
|
source: result.source === "manual" ? "manual" : "code",
|
|
1812
|
+
result_type: resultType,
|
|
1679
1813
|
uri: String(result.uri ?? ""),
|
|
1680
1814
|
title: String(result.title ?? ""),
|
|
1681
|
-
|
|
1682
|
-
score: result.score ?? 0,
|
|
1815
|
+
preview: buildPreview(result, query),
|
|
1816
|
+
score: (result.score ?? 0) + rerankBoost,
|
|
1683
1817
|
...referencedUris ? { referenced_files: referencedUris } : {},
|
|
1684
1818
|
classification_summary: toSummary(details),
|
|
1685
1819
|
...include_details ? { classification_details: details } : {}
|
|
@@ -1687,7 +1821,7 @@ async function executeSearch(context) {
|
|
|
1687
1821
|
};
|
|
1688
1822
|
})
|
|
1689
1823
|
);
|
|
1690
|
-
const hits = mapped.filter((entry) => matchesSystemFilter(entry.hit.classification_summary.systems, system)).sort((a, b) => b.score - a.score || a.hit.uri.localeCompare(b.hit.uri)).slice(0, limit);
|
|
1824
|
+
const hits = mapped.filter((entry) => requestedType === "all" || entry.hit.result_type === requestedType).filter((entry) => matchesSystemFilter(entry.hit.classification_summary.systems, system)).sort((a, b) => b.score - a.score || a.hit.uri.localeCompare(b.hit.uri)).slice(0, limit);
|
|
1691
1825
|
return {
|
|
1692
1826
|
ok: true,
|
|
1693
1827
|
data: {
|