zigsm 1.4.0 → 1.4.2

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 CHANGED
@@ -12,7 +12,7 @@ It uses the same approach as Zig's official autodoc (ziglang.org) by reading STD
12
12
  By default, the server uses your locally installed Zig compiler to serve documentation, ensuring you always get docs that match your actual Zig version. It can also fetch documentation from ziglang.org if needed.
13
13
 
14
14
  > [!TIP]
15
- > Add `use zigdocs` to your prompt if you want to explicitly instruct the LLM to use Zig docs tools. Otherwise, LLM will automatically decide when to utilize MCP tools based on the context of your questions.
15
+ > Add `use zsm` to your prompt if you want to explicitly instruct the LLM to use zsm tools. Otherwise, LLM will automatically decide when to utilize MCP tools based on the context of your questions.
16
16
 
17
17
  <p align="center" width="100%">
18
18
  <img src="https://raw.githubusercontent.com/zig-wasm/.github/refs/heads/main/static/readme_mcp_1.gif" width="49%" />
@@ -82,7 +82,7 @@ When using `--doc-source remote`, documentation is fetched from ziglang.org and
82
82
 
83
83
  Optional environment variables:
84
84
  - `VOYAGE_API_KEY` - Enables embeddings for doc search when set.
85
- - `VOYAGE_MODEL` - Embedding model override (default: `voyage-3-lite`).
85
+ - `VOYAGE_MODEL` - Embedding model override (default: `voyage-code-3`).
86
86
 
87
87
  Embeddings are cached per Zig version, doc source, and model in the same cache directory used for docs.
88
88
 
@@ -117,13 +117,19 @@ Add the JSON configuration below to your MCP settings file.
117
117
 
118
118
  ### JSON Configuration Template
119
119
 
120
+ To enable semantic search with Voyage embeddings, include an `env` block like shown below.
121
+
120
122
  **Node.js:**
121
123
  ```json
122
124
  {
123
125
  "mcpServers": {
124
126
  "zsm": {
125
127
  "command": "npx",
126
- "args": ["-y", "zigsm@latest"]
128
+ "args": ["-y", "zigsm@latest"],
129
+ "env": {
130
+ "VOYAGE_API_KEY": "your_voyage_api_key",
131
+ "VOYAGE_MODEL": "voyage-code-3"
132
+ }
127
133
  }
128
134
  }
129
135
  }
@@ -135,7 +141,11 @@ Add the JSON configuration below to your MCP settings file.
135
141
  "mcpServers": {
136
142
  "zsm": {
137
143
  "command": "bunx",
138
- "args": ["zigsm@latest"]
144
+ "args": ["zigsm@latest"],
145
+ "env": {
146
+ "VOYAGE_API_KEY": "your_voyage_api_key",
147
+ "VOYAGE_MODEL": "voyage-code-3"
148
+ }
139
149
  }
140
150
  }
141
151
  }
@@ -161,7 +171,11 @@ claude mcp add zsm -- bunx zigsm@latest --doc-source remote --version 0.14.1
161
171
  "mcpServers": {
162
172
  "zsm": {
163
173
  "command": "npx",
164
- "args": ["-y", "zigsm@latest", "--doc-source", "remote", "--version", "master"]
174
+ "args": ["-y", "zigsm@latest", "--doc-source", "remote", "--version", "master"],
175
+ "env": {
176
+ "VOYAGE_API_KEY": "your_voyage_api_key",
177
+ "VOYAGE_MODEL": "voyage-code-3"
178
+ }
165
179
  }
166
180
  }
167
181
  }
@@ -173,7 +187,11 @@ claude mcp add zsm -- bunx zigsm@latest --doc-source remote --version 0.14.1
173
187
  "mcpServers": {
174
188
  "zsm": {
175
189
  "command": "bunx",
176
- "args": ["zigsm@latest", "--doc-source", "remote", "--version", "0.14.1"]
190
+ "args": ["zigsm@latest", "--doc-source", "remote", "--version", "0.14.1"],
191
+ "env": {
192
+ "VOYAGE_API_KEY": "your_voyage_api_key",
193
+ "VOYAGE_MODEL": "voyage-code-3"
194
+ }
177
195
  }
178
196
  }
179
197
  }
package/dist/mcp.js CHANGED
@@ -103,7 +103,7 @@ async function main() {
103
103
  const builtinFunctions = await ensureDocs(options.version, options.updatePolicy, true, options.docSource);
104
104
  const stdSources = await downloadSourcesTar(options.version, true, false, options.docSource);
105
105
  const mcpServer = new McpServer({
106
- name: "ZigDocs",
106
+ name: "ZSM",
107
107
  description: "Retrieves up-to-date documentation for the Zig programming language standard library and builtin functions.",
108
108
  version: options.version,
109
109
  });
package/dist/std.js CHANGED
@@ -741,6 +741,38 @@ function setInputString(s) {
741
741
  wasmArray.set(jsArray);
742
742
  }
743
743
  let inMemoryEmbeddingCache = null;
744
+ let cachedSourceTextIndex = null;
745
+ let cachedSourceTextRef = null;
746
+ function countWordHits(text, words) {
747
+ let hits = 0;
748
+ for (const word of words) {
749
+ if (text.includes(word))
750
+ hits++;
751
+ }
752
+ return hits;
753
+ }
754
+ function scoreDeclMatch(decl, queryLower, queryWords) {
755
+ const nameLower = decl.name.toLowerCase();
756
+ const pathLower = decl.path.toLowerCase();
757
+ let score = 0;
758
+ if (nameLower === queryLower)
759
+ score += 1000;
760
+ else if (nameLower.startsWith(queryLower))
761
+ score += 700;
762
+ else if (nameLower.includes(queryLower))
763
+ score += 450;
764
+ if (pathLower.includes(queryLower))
765
+ score += 150;
766
+ if (score === 0 && queryWords.length > 0) {
767
+ const searchable = `${nameLower} ${decl.preview.toLowerCase()} ${pathLower}`;
768
+ const wordHits = countWordHits(searchable, queryWords);
769
+ if (wordHits === queryWords.length)
770
+ score += 300;
771
+ else if (wordHits > 0)
772
+ score += wordHits * 80;
773
+ }
774
+ return score;
775
+ }
744
776
  async function initWasmRuntime(wasmPath, stdSources) {
745
777
  const fs = await import("node:fs");
746
778
  const wasmBytes = typeof wasmPath === "string" ? fs.readFileSync(wasmPath) : wasmPath;
@@ -813,6 +845,169 @@ function stripMarkdown(input) {
813
845
  .replace(/\s+/g, " ")
814
846
  .trim();
815
847
  }
848
+ function parseTarOctal(raw) {
849
+ const cleaned = raw.replace(/\0/g, "").trim();
850
+ if (!cleaned)
851
+ return 0;
852
+ const parsed = Number.parseInt(cleaned, 8);
853
+ return Number.isFinite(parsed) ? parsed : 0;
854
+ }
855
+ function parseSourcesTar(stdSources) {
856
+ const files = [];
857
+ let offset = 0;
858
+ while (offset + 512 <= stdSources.length) {
859
+ const header = stdSources.subarray(offset, offset + 512);
860
+ let emptyHeader = true;
861
+ for (let i = 0; i < 512; i++) {
862
+ if (header[i] !== 0) {
863
+ emptyHeader = false;
864
+ break;
865
+ }
866
+ }
867
+ if (emptyHeader)
868
+ break;
869
+ const name = text_decoder.decode(header.subarray(0, 100)).replace(/\0.*$/, "").trim();
870
+ const sizeRaw = text_decoder
871
+ .decode(header.subarray(124, 136))
872
+ .replace(/\0.*$/, "")
873
+ .trim();
874
+ const prefix = text_decoder
875
+ .decode(header.subarray(345, 500))
876
+ .replace(/\0.*$/, "")
877
+ .trim();
878
+ const size = parseTarOctal(sizeRaw);
879
+ const fullPath = prefix.length > 0 ? `${prefix}/${name}` : name;
880
+ const contentStart = offset + 512;
881
+ const contentEnd = contentStart + size;
882
+ if (size > 0 && contentEnd <= stdSources.length && fullPath.endsWith(".zig")) {
883
+ const text = text_decoder.decode(stdSources.subarray(contentStart, contentEnd));
884
+ files.push({ path: fullPath, text, lowerText: text.toLowerCase() });
885
+ }
886
+ const paddedSize = Math.ceil(size / 512) * 512;
887
+ offset = contentStart + paddedSize;
888
+ }
889
+ return files;
890
+ }
891
+ function buildSourceTextIndex(stdSources) {
892
+ if (cachedSourceTextIndex && cachedSourceTextRef === stdSources) {
893
+ return cachedSourceTextIndex;
894
+ }
895
+ const files = parseSourcesTar(stdSources);
896
+ const decls = [];
897
+ const declRegex = /^\s*(?:pub\s+)?(?:const|var|fn)\s+([A-Za-z_][A-Za-z0-9_]*)/;
898
+ for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
899
+ const file = files[fileIndex];
900
+ const lines = file.text.split("\n");
901
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
902
+ const line = lines[lineIndex];
903
+ const match = line.match(declRegex);
904
+ if (!match)
905
+ continue;
906
+ decls.push({
907
+ name: match[1],
908
+ path: file.path,
909
+ line: lineIndex + 1,
910
+ preview: line.trim(),
911
+ });
912
+ }
913
+ }
914
+ cachedSourceTextRef = stdSources;
915
+ cachedSourceTextIndex = { files, decls };
916
+ return cachedSourceTextIndex;
917
+ }
918
+ function fallbackSearchStdLib(stdSources, query, limit) {
919
+ const index = buildSourceTextIndex(stdSources);
920
+ const queryLower = query.toLowerCase().trim();
921
+ const queryWords = queryLower.split(/\s+/).filter(Boolean);
922
+ const declScored = index.decls
923
+ .map((decl) => ({ decl, score: scoreDeclMatch(decl, queryLower, queryWords) }))
924
+ .filter((item) => item.score > 0)
925
+ .sort((a, b) => b.score - a.score);
926
+ const contentMatches = [];
927
+ if (declScored.length < limit) {
928
+ for (const file of index.files) {
929
+ const lines = file.text.split("\n");
930
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
931
+ const wordHits = countWordHits(lines[lineIndex].toLowerCase(), queryWords);
932
+ if (wordHits === queryWords.length) {
933
+ contentMatches.push({
934
+ path: file.path,
935
+ line: lineIndex + 1,
936
+ preview: lines[lineIndex].trim().slice(0, 120),
937
+ score: 200 + wordHits * 30,
938
+ });
939
+ }
940
+ if (contentMatches.length >= limit * 3)
941
+ break;
942
+ }
943
+ if (contentMatches.length >= limit * 3)
944
+ break;
945
+ }
946
+ contentMatches.sort((a, b) => b.score - a.score);
947
+ }
948
+ let markdown = `# Search Results\n\nQuery: "${query}"\n\n`;
949
+ markdown += `_Fallback mode: text index (parser-incompatible Zig version)_\n\n`;
950
+ if (declScored.length === 0 && contentMatches.length === 0) {
951
+ markdown += "No results found.";
952
+ return markdown;
953
+ }
954
+ if (declScored.length > 0) {
955
+ const limited = declScored.slice(0, limit);
956
+ markdown += `Found ${declScored.length} declaration matches (showing ${limited.length}):\n\n`;
957
+ for (let i = 0; i < limited.length; i++) {
958
+ const entry = limited[i].decl;
959
+ markdown += `- std.${entry.name} (${entry.path}:${entry.line})\n`;
960
+ }
961
+ }
962
+ if (contentMatches.length > 0 && declScored.length < limit) {
963
+ const remaining = limit - declScored.length;
964
+ const limited = contentMatches.slice(0, remaining);
965
+ markdown += `\nContent matches:\n\n`;
966
+ for (let i = 0; i < limited.length; i++) {
967
+ const entry = limited[i];
968
+ markdown += `- ${entry.path}:${entry.line} — \`${entry.preview}\`\n`;
969
+ }
970
+ }
971
+ return markdown;
972
+ }
973
+ function fallbackGetStdLibItem(stdSources, name, getSourceFile) {
974
+ const index = buildSourceTextIndex(stdSources);
975
+ if (getSourceFile) {
976
+ const normalized = name.replace(/^src\//, "");
977
+ const matchedFile = index.files.find((file) => file.path === normalized) ||
978
+ index.files.find((file) => file.path.endsWith(`/${normalized}`)) ||
979
+ index.files.find((file) => file.path.endsWith(name));
980
+ if (!matchedFile) {
981
+ return `# Error\n\nCould not find source file for "${name}" in fallback mode.`;
982
+ }
983
+ return `# ${matchedFile.path}\n\n${matchedFile.text}`;
984
+ }
985
+ const target = name.split(".").pop()?.trim() || name.trim();
986
+ const targetLower = target.toLowerCase();
987
+ const ranked = index.decls
988
+ .map((decl) => ({ decl, score: scoreDeclMatch(decl, targetLower, [targetLower]) }))
989
+ .filter((item) => item.score > 0)
990
+ .sort((a, b) => b.score - a.score);
991
+ if (ranked.length === 0) {
992
+ return `# Error\n\nDeclaration "${name}" not found (fallback mode).`;
993
+ }
994
+ const best = ranked[0].decl;
995
+ const file = index.files.find((entry) => entry.path === best.path);
996
+ if (!file) {
997
+ return `# Error\n\nDeclaration "${name}" matched, but source file could not be loaded.`;
998
+ }
999
+ const lines = file.text.split("\n");
1000
+ const startLine = Math.max(1, best.line - 20);
1001
+ const endLine = Math.min(lines.length, best.line + 80);
1002
+ const snippet = lines.slice(startLine - 1, endLine).join("\n");
1003
+ let markdown = `# ${name}\n\n`;
1004
+ markdown += `_Fallback mode: text index (parser-incompatible Zig version)_\n\n`;
1005
+ markdown += `Match: ${best.path}:${best.line}\n\n`;
1006
+ markdown += "```zig\n";
1007
+ markdown += snippet;
1008
+ markdown += "\n```\n";
1009
+ return markdown;
1010
+ }
816
1011
  function buildDeclEmbeddingText(decl) {
817
1012
  const category = wasm_exports.categorize_decl(decl, 0);
818
1013
  const fqn = fullyQualifiedName(decl);
@@ -832,6 +1027,14 @@ function buildDeclEmbeddingText(decl) {
832
1027
  const text = stripMarkdown(`${fqn}\n${name}\n${proto}\n${typeInfo}\n${docs}\n${source}`).slice(0, 4000);
833
1028
  return { decl, fqn, text };
834
1029
  }
1030
+ function tryBuildDeclEmbeddingText(decl) {
1031
+ try {
1032
+ return buildDeclEmbeddingText(decl);
1033
+ }
1034
+ catch {
1035
+ return null;
1036
+ }
1037
+ }
835
1038
  async function getEmbeddingCachePath(zigVersion, docSource, model) {
836
1039
  const envPathsMod = await import("env-paths");
837
1040
  const path = await import("node:path");
@@ -891,9 +1094,14 @@ async function getOrBuildEmbeddingCache(wasmPath, stdSources, options) {
891
1094
  inMemoryEmbeddingCache = existing;
892
1095
  return existing;
893
1096
  }
894
- await initWasmRuntime(wasmPath, stdSources);
895
1097
  const decls = collectDeclsForEmbeddings();
896
- const docs = decls.map((decl) => buildDeclEmbeddingText(decl)).filter((doc) => doc.text.length > 0);
1098
+ const docs = [];
1099
+ for (let i = 0; i < decls.length; i++) {
1100
+ const doc = tryBuildDeclEmbeddingText(decls[i]);
1101
+ if (doc && doc.text.length > 0) {
1102
+ docs.push(doc);
1103
+ }
1104
+ }
897
1105
  if (docs.length === 0) {
898
1106
  return null;
899
1107
  }
@@ -929,7 +1137,12 @@ function buildHybridRanking(lexicalDecls, semanticDecls, maxResults) {
929
1137
  .map(([decl]) => decl);
930
1138
  }
931
1139
  export async function searchStdLib(wasmPath, stdSources, query, limit = 20, options = {}) {
932
- await initWasmRuntime(wasmPath, stdSources);
1140
+ try {
1141
+ await initWasmRuntime(wasmPath, stdSources);
1142
+ }
1143
+ catch {
1144
+ return fallbackSearchStdLib(stdSources, query, limit);
1145
+ }
933
1146
  const ignoreCase = query.toLowerCase() === query;
934
1147
  const lexicalResults = Array.from(executeQuery(query, ignoreCase));
935
1148
  let mergedResults = lexicalResults;
@@ -968,7 +1181,12 @@ export async function searchStdLib(wasmPath, stdSources, query, limit = 20, opti
968
1181
  return markdown;
969
1182
  }
970
1183
  export async function getStdLibItem(wasmPath, stdSources, name, getSourceFile = false) {
971
- await initWasmRuntime(wasmPath, stdSources);
1184
+ try {
1185
+ await initWasmRuntime(wasmPath, stdSources);
1186
+ }
1187
+ catch {
1188
+ return fallbackGetStdLibItem(stdSources, name, getSourceFile);
1189
+ }
972
1190
  const exports = wasm_exports;
973
1191
  const decl_index = findDecl(name);
974
1192
  if (decl_index === null) {
package/dist/tools.js CHANGED
@@ -67,15 +67,26 @@ function getBuiltinFunctionTool(builtinFunctions) {
67
67
  ],
68
68
  };
69
69
  }
70
+ const queryBare = queryLower.replace(/@/g, "");
71
+ const queryWords = queryBare.split(/\s+/).filter(Boolean);
70
72
  const scoredFunctions = builtinFunctions.map((fn) => {
71
73
  const funcLower = fn.func.toLowerCase();
74
+ const funcBare = funcLower.replace(/^@/, "");
72
75
  let score = 0;
73
- if (funcLower === queryLower)
76
+ if (funcLower === queryLower || funcBare === queryBare)
74
77
  score += 1000;
75
78
  else if (funcLower.startsWith(queryLower))
76
79
  score += 500;
77
80
  else if (funcLower.includes(queryLower))
78
81
  score += 300;
82
+ if (score === 0 && queryWords.length > 0) {
83
+ const searchable = `${funcBare} ${fn.docs.toLowerCase()} ${fn.signature.toLowerCase()}`;
84
+ const wordHits = queryWords.filter((w) => searchable.includes(w)).length;
85
+ if (wordHits === queryWords.length)
86
+ score += 200;
87
+ else if (wordHits > 0)
88
+ score += wordHits * 60;
89
+ }
79
90
  if (score > 0)
80
91
  score += Math.max(0, 50 - fn.func.length);
81
92
  return { ...fn, score };
package/dist/voyage.js CHANGED
@@ -6,7 +6,7 @@ export function getVoyageConfig() {
6
6
  }
7
7
  return {
8
8
  apiKey,
9
- model: env?.VOYAGE_MODEL?.trim() || "voyage-3-lite",
9
+ model: env?.VOYAGE_MODEL?.trim() || "voyage-code-3",
10
10
  };
11
11
  }
12
12
  async function embedBatch(input, config, inputType) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zigsm",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zsm": "dist/mcp.js"
@@ -24,14 +24,14 @@
24
24
  "description": "MCP server that provides up-to-date documentation for the Zig programming language standard library and builtin functions.",
25
25
  "repository": {
26
26
  "type": "git",
27
- "url": "git+https://github.com/zig-wasm/zig-mcp.git"
27
+ "url": "git+https://github.com/pokeylooted/zsm.git"
28
28
  },
29
29
  "keywords": [
30
30
  "mcp",
31
31
  "modelcontextprotocol",
32
32
  "zig",
33
33
  "ziglang",
34
- "zig-docs",
34
+ "zsm",
35
35
  "cli"
36
36
  ],
37
37
  "dependencies": {