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.
Maintainers
Readme
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,
#baseincludes, 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#baseincludes. - CLI included -
parse,stringify,formatandget, with KV1/KV3 auto-detection.
Installation
npm install keyvalues-tools
pnpm add keyvalues-tools
bun add keyvalues-toolsQuick 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 headerVerified 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 pathsWithout 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,\tescapes by default, which mangles literal paths like"c:\newmaps". Pass{ escapeSequences: false }(CLI--raw) to keep backslashes literal.Errors report
line L, column Cfor 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-checkContributing
Please check CONTRIBUTING.md for details.
License
Distributed under the MIT License. See LICENSE for more information.
