Skip to content

K4ryuu/keyvalues-tools

Repository files navigation

NPM Version GitHub License TypeScript Bundle Size


keyvalues-tools

Ultra-fast, zero-dependency parser and serializer for Valve KeyValues (VDF / KV1)
KV1 / VDF + KV3 (Source 2 / CS2) • Zero runtime dependencies • Pure TypeScript • Comment & Conditional Preservation • CLI

Get Started »

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

(back to top)

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.

(back to top)

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.

(back to top)

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.

About

Zero-dependency TypeScript parser and serializer for Valve KeyValues (KV1/VDF + KV3). Read, edit, validate and convert Source engine config, with a CLI.

Topics

Resources

License

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Sponsor this project

 

Contributors