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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/stdio.js +147 -13
  3. 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`, `snippet`, `score`, `classification_summary`), optional `referenced_files` as `code://...` URIs readable by `read_uri`, and optional `classification_details`; `system` defaults to `c64` and supports `all` for cross-system results
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
- snippet: z.string().describe("Preview text to evaluate relevance before reading."),
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", "snippet", "classification", "referencedFiles"],
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
- snippet: makeExcerpt(body),
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
- const files = await listFilesRecursive(root.absRoot);
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
- uri: root.scope === "sample" && !uriRel.startsWith("samples/") ? codeUri("oscar", uriRel) : codeUri(root.scope, uriRel),
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
- snippet: makeExcerpt(text, 700),
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
- snippet: String(result.snippet ?? ""),
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: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oscar64-mcp-docs",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [