npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

keyvalues-tools

v1.0.1

Published

Zero-dependency TypeScript parser and serializer for Valve KeyValues (VDF / KV1), for Node.js and Bun. Comments, platform conditionals, #base includes, duplicate-key arrays, schema validation and a CLI.

Readme

NPM Version GitHub License TypeScript Bundle Size

About The Project

Valve uses KeyValues for everything: game configuration (gameinfo.gi), materials, SourceMod configs, localization, layouts, and, in Source 2 / Counter-Strike 2, bot behavior trees and data files.

This library is a modern, lightweight, type-safe implementation of both Valve KeyValues formats for Node.js and Bun, with zero runtime dependencies:

  • KV1 / VDF (Source 1, SourceMod, gameinfo): full parser + serializer, comment/order-preserving AST, platform conditionals, #base includes, duplicate-key arrays, and schema validation.
  • KV3 (Source 2 / CS2): text-format parser + serializer (typed values, arrays, #base-style objects, multi-line strings, flags), verified against real CS2 behavior-tree files.

There is no real "KV2" format (it is a common naming myth), so this package does not claim it. Binary KV3 encodings are not supported yet.

Why this package is special

  • Zero runtime dependencies - No heavy parser libraries or helper utilities.
  • Both KeyValues formats - KV1/VDF and KV3 (Source 2 / CS2), auto-detected by the KV3 header.
  • Handles duplicate keys - Group duplicates into arrays (e.g. search paths) OR detailed AST parsing that preserves order and comments.
  • Comment-tolerant - Ignores //, /* */, and KV3 <!-- --> comments.
  • SteamID-safe - Type conversion keeps 64-bit IDs and oversized integers as strings instead of losing precision.
  • Conditionals & #base - Reads platform conditionals like [!$OSX] and resolves #base includes.
  • CLI included - parse, stringify, format and get, with KV1/KV3 auto-detection.

Installation

npm install keyvalues-tools
pnpm add keyvalues-tools
bun add keyvalues-tools

Quick example

1. Basic parsing & serialization

import { parse, stringify } from "keyvalues-tools";

const vdf = `
"GameInfo"
{
  "game"  "Counter-Strike 2"
  "title" "Counter-Strike"
}
`;

// Parse into JS Object
const obj = parse(vdf);
console.log(obj.GameInfo.game); // "Counter-Strike 2"

// Stringify back to VDF
const serialized = stringify(obj);
console.log(serialized);

2. Handling duplicate keys (e.g., search paths)

By default, later duplicate keys overwrite earlier ones. Set { duplicates: "array" } to group them:

import { parse } from "keyvalues-tools";

const searchPaths = `
"SearchPaths"
{
  "Game" "csgo"
  "Game" "core"
}
`;

const obj = parse(searchPaths, { duplicates: "array" });
console.log(obj.SearchPaths.Game); // [ "csgo", "core" ]

3. Detailed AST parsing (preserving comments, order & conditionals)

For tools that need to inspect platform conditionals, edit values, or reconstruct the original VDF exactly with all comments:

import { parseDetailed, stringifyDetailed } from "keyvalues-tools";

const vdf = `
  // This is a header comment
  "addons"
  {
    "metamod" "1.12.0" [!$OSX] // Windows/Linux only
  }
`;

const ast = parseDetailed(vdf);
console.log(ast[0]);
// {
//   key: "addons",
//   comment: "This is a header comment",
//   value: [
//     {
//       key: "metamod",
//       value: "1.12.0",
//       conditional: "!$OSX",
//       inlineComment: "Windows/Linux only"
//     }
//   ]
// }

// Modify values in AST and write back with comments preserved exactly as they were!
const serialized = stringifyDetailed(ast);

4. Automatic Type Conversion

Convert values that look like numbers or booleans automatically into their JS types:

import { parse } from "keyvalues-tools";

const vdf = `
"Config"
{
  "enabled" "true"
  "count"   "64"
}
`;

const obj = parse(vdf, { convertTypes: true });
console.log(obj.Config.enabled); // true (boolean)
console.log(obj.Config.count);   // 64 (number)

5. Platform Conditionals Evaluation

Filter out unmatched platform keys or blocks when parsing to a standard JS Object:

import { parse } from "keyvalues-tools";

const vdf = `
"addons"
{
  "metamod" "1.12.0" [!$OSX]
  "metamod" "1.12.0-mac" [$OSX]
}
`;

// Evaluates standard platform tags: 'win32' | 'linux' | 'darwin'
const obj = parse(vdf, { evaluateConditionals: "darwin" });
console.log(obj.addons.metamod); // "1.12.0-mac"

6. Recursive #base Imports

Resolve and recursively merge #base files (imports) into the main KeyValues object structure:

import { parse } from "keyvalues-tools";
import { readFileSync } from "fs";
import { join } from "path";

const mainVdf = `
  #base "common.vdf"
  "Config"
  {
    "name" "Main Config"
  }
`;

const obj = parse(mainVdf, {
  resolveBase: (basePath) => {
    // Return base file string or pre-parsed KVObject
    return readFileSync(join(__dirname, basePath), "utf-8");
  }
});

7. Schema Validation using KVValidator

Validate a parsed VDF object against a schema structure (similar to Zod/schema validation):

import { parse, KVValidator } from "keyvalues-tools";

const config = parse(vdfString);
const schema = {
  GameInfo: {
    required: true,
    type: {
      game: { type: "string", required: true },
      maxplayers: { type: "number", required: true },
      lan: { type: "boolean", required: false }
    }
  }
};

const errors = KVValidator.validate(config, schema);
if (errors.length > 0) {
  console.error("Validation failed:", errors);
}

8. KeyValues3 (Source 2 / CS2)

Parse and serialize the KV3 text format used by Source 2 games. Input is auto-detected from the <!-- kv3 ... --> header via isKV3.

import { parseKV3, stringifyKV3, isKV3, get } from "keyvalues-tools";

const kv3 = `<!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:generic:version{7412167c-06e9-4698-aff2-e63eb59037e7} -->
{
  difficulty = "hard"
  aim_max_duration = 0.9
  botId = 76561198012345678       // kept as a string, no precision loss
  weapons = [ "ak47", "awp", ]    // trailing comma allowed
  icon = resource:"materials/x.vmat"
}`;

const data = parseKV3(kv3);
console.log(get(data, "weapons"));    // ["ak47", "awp"]
console.log(typeof data.botId);        // "string" (SteamID-safe)

const text = stringifyKV3(data);       // back to KV3 with the standard header

Verified against real CS2 bot behavior-tree files. Binary KV3 encodings are not supported.

9. CLI

keyvalues-tools parse gameinfo.gi gameinfo.json --convert-types   # VDF or KV3 -> JSON (auto-detected)
keyvalues-tools stringify data.json data.vdf                      # JSON -> VDF (KV1)
keyvalues-tools format addoninfo.txt                              # normalize / pretty-print (comments preserved)
keyvalues-tools format addoninfo.txt --check                      # CI: exit 1 if not already formatted
keyvalues-tools get gameinfo.gi GameInfo.FileSystem.SearchPaths   # read a value by dot path
keyvalues-tools validate gameinfo.gi schema.json                  # validate against a schema
keyvalues-tools parse paths.vdf --raw                             # keep literal Windows paths

Without an output path, results print to stdout. Also runnable via npx keyvalues-tools.

API

| Member | Description | |---|---| | parse(text, options?) | KV1/VDF to a JS object. Options: duplicates, convertTypes, evaluateConditionals, resolveBase, escapeSequences | | parseDetailed(text, options?) | KV1 to an ordered KVNode[] AST preserving comments, order, duplicates, conditionals | | stringify(obj, options?) / stringifyDetailed(nodes, options?) | JS object / AST back to KV1 text. Options: indent, forceQuotes, escapeSequences | | parseKV3(text, options?) | KV3 (Source 2 / CS2) text to a JS value. { preserveFlags: true } keeps resource:/subclass: flags as KV3Flagged | | parseKV3Detailed(text) / stringifyKV3Detailed(nodes, options?) | KV3 comment/flag/order-preserving KV3Node[] AST for format-aware edits (object-rooted docs) | | stringifyKV3(value, options?) | JS value to KV3 text (with the standard header) | | isKV3(text) | Detect KV3 vs KV1 from the header | | get(obj, "a.b.c") / set(obj, "a.b.c", value) | Read (KV1 or KV3) or write a nested value by dot path | | diff(a, b) / merge(base, override) | Compare two objects ({ added, removed, changed }) or deep-merge them (override wins) | | parseFile(path, options?) / parseKV3File(path) | Read a file and parse it; parseFile auto-resolves #base relative to the file | | KVValidator.validate(obj, schema) | Validate a parsed object against a schema, returns ValidationError[] |

Windows paths: VDF processes \n, \t escapes by default, which mangles literal paths like "c:\newmaps". Pass { escapeSequences: false } (CLI --raw) to keep backslashes literal.

Errors report line L, column C for quick debugging of malformed files.

All types (KVObject, KVValue, KVNode, KVDiff, ParseOptions, StringifyOptions, KV3Value, KV3Object, KV3Node, KV3ParseOptions, SchemaObject, ...) are exported.

Examples

Self-contained, one topic each (see examples/README.md):

bun run examples/parse.ts              # VDF/KV1 -> JS object, type conversion
bun run examples/duplicates-ast.ts     # duplicate keys as arrays + comment-preserving AST
bun run examples/conditionals-base.ts  # platform conditionals + #base includes
bun run examples/validate.ts           # schema validation
bun run examples/edit.ts               # get / set / diff + literal Windows paths
bun run examples/kv3.ts                # KeyValues3 (Source 2 / CS2)

Testing

bun test              # unit tests incl. KV1, KV3, the official VDC example, and a committed real-shaped fixture
bun run fetch-fixtures # optional: download real KV3 fixtures into test/fixtures/real (gitignored)
bun run lint
bun run type-check

Contributing

Please check CONTRIBUTING.md for details.

License

Distributed under the MIT License. See LICENSE for more information.