toolnexus 0.2.0 → 0.2.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/dist/skill.js +20 -12
  2. package/package.json +26 -8
  3. package/src/skill.ts +19 -12
package/dist/skill.js CHANGED
@@ -7,6 +7,7 @@
7
7
  import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
8
8
  import path from "node:path";
9
9
  import { pathToFileURL } from "node:url";
10
+ import { parse as parseYaml } from "yaml";
10
11
  export const SKILL_TOOL_DESCRIPTION = `Load a specialized skill when the task at hand matches one of the skills listed in the system prompt.
11
12
 
12
13
  Use this tool to inject the skill's instructions and resources into current conversation. The output may contain detailed workflow guidance as well as references to scripts, files, etc in the same directory as the skill.
@@ -18,24 +19,31 @@ The skill name must match one of the skills listed in your system prompt.`;
18
19
  */
19
20
  export const SKILLS_PROMPT_PREAMBLE = "Skills provide specialized instructions and workflows for specific tasks.\n" +
20
21
  "Use the skill tool to load a skill when a task matches its description.";
21
- /** Minimal YAML frontmatter parser — only flat `key: value` pairs are needed. */
22
+ /**
23
+ * Parse YAML frontmatter (between the leading `---` fences) with a real YAML
24
+ * parser, so folded (`>`)/literal (`|`) block scalars, quoting, and multi-line
25
+ * values all resolve correctly. Scalar values are coerced to strings.
26
+ */
22
27
  function parseFrontmatter(text) {
23
28
  const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text);
24
29
  if (!match)
25
30
  return { data: {}, content: text };
26
31
  const data = {};
27
- for (const line of match[1].split(/\r?\n/)) {
28
- const idx = line.indexOf(":");
29
- if (idx === -1)
30
- continue;
31
- const key = line.slice(0, idx).trim();
32
- let value = line.slice(idx + 1).trim();
33
- if ((value.startsWith('"') && value.endsWith('"')) ||
34
- (value.startsWith("'") && value.endsWith("'"))) {
35
- value = value.slice(1, -1);
32
+ let parsed;
33
+ try {
34
+ parsed = parseYaml(match[1]);
35
+ }
36
+ catch {
37
+ parsed = null;
38
+ }
39
+ if (parsed && typeof parsed === "object") {
40
+ for (const [key, value] of Object.entries(parsed)) {
41
+ // Trim so block-scalar trailing newlines (chomping differs subtly between
42
+ // YAML libs) don't leak — keeps the five ports byte-identical.
43
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
44
+ data[key] = String(value).trim();
45
+ }
36
46
  }
37
- if (key)
38
- data[key] = value;
39
47
  }
40
48
  return { data, content: match[2] };
41
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolnexus",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Dynamic MCP servers + agent skills as uniform tools for any LLM (opencode-style).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -11,22 +11,40 @@
11
11
  "import": "./dist/index.js"
12
12
  }
13
13
  },
14
- "files": ["dist", "src"],
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
15
18
  "scripts": {
16
19
  "build": "tsc -p tsconfig.json",
17
20
  "test": "npm run build && node --experimental-strip-types --test test/unit.test.ts",
18
21
  "example": "node --experimental-strip-types examples/basic.ts"
19
22
  },
20
- "keywords": ["mcp", "llm", "agent", "skills", "tools", "openai", "anthropic", "gemini"],
23
+ "keywords": [
24
+ "mcp",
25
+ "llm",
26
+ "agent",
27
+ "skills",
28
+ "tools",
29
+ "openai",
30
+ "anthropic",
31
+ "gemini"
32
+ ],
21
33
  "license": "MIT",
22
- "repository": { "type": "git", "url": "git+https://github.com/muthuishere/toolnexus.git" },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/muthuishere/toolnexus.git"
37
+ },
23
38
  "homepage": "https://github.com/muthuishere/toolnexus#readme",
24
- "bugs": { "url": "https://github.com/muthuishere/toolnexus/issues" },
39
+ "bugs": {
40
+ "url": "https://github.com/muthuishere/toolnexus/issues"
41
+ },
25
42
  "dependencies": {
26
- "@modelcontextprotocol/sdk": "^1.12.0"
43
+ "@modelcontextprotocol/sdk": "^1.12.0",
44
+ "yaml": "^2.9.0"
27
45
  },
28
46
  "devDependencies": {
29
- "typescript": "^5.6.0",
30
- "@types/node": "^22.0.0"
47
+ "@types/node": "^22.0.0",
48
+ "typescript": "^5.6.0"
31
49
  }
32
50
  }
package/src/skill.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs"
8
8
  import path from "node:path"
9
9
  import { pathToFileURL } from "node:url"
10
+ import { parse as parseYaml } from "yaml"
10
11
  import type { Tool, ToolResult } from "./types.js"
11
12
 
12
13
  export const SKILL_TOOL_DESCRIPTION = `Load a specialized skill when the task at hand matches one of the skills listed in the system prompt.
@@ -30,23 +31,29 @@ export interface SkillInfo {
30
31
  content: string // body after frontmatter
31
32
  }
32
33
 
33
- /** Minimal YAML frontmatter parser — only flat `key: value` pairs are needed. */
34
+ /**
35
+ * Parse YAML frontmatter (between the leading `---` fences) with a real YAML
36
+ * parser, so folded (`>`)/literal (`|`) block scalars, quoting, and multi-line
37
+ * values all resolve correctly. Scalar values are coerced to strings.
38
+ */
34
39
  function parseFrontmatter(text: string): { data: Record<string, string>; content: string } {
35
40
  const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text)
36
41
  if (!match) return { data: {}, content: text }
37
42
  const data: Record<string, string> = {}
38
- for (const line of match[1].split(/\r?\n/)) {
39
- const idx = line.indexOf(":")
40
- if (idx === -1) continue
41
- const key = line.slice(0, idx).trim()
42
- let value = line.slice(idx + 1).trim()
43
- if (
44
- (value.startsWith('"') && value.endsWith('"')) ||
45
- (value.startsWith("'") && value.endsWith("'"))
46
- ) {
47
- value = value.slice(1, -1)
43
+ let parsed: unknown
44
+ try {
45
+ parsed = parseYaml(match[1])
46
+ } catch {
47
+ parsed = null
48
+ }
49
+ if (parsed && typeof parsed === "object") {
50
+ for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
51
+ // Trim so block-scalar trailing newlines (chomping differs subtly between
52
+ // YAML libs) don't leak — keeps the five ports byte-identical.
53
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
54
+ data[key] = String(value).trim()
55
+ }
48
56
  }
49
- if (key) data[key] = value
50
57
  }
51
58
  return { data, content: match[2] }
52
59
  }