An AI agent skill, a cross-platform CLI tool, and a C# class for working with TPS files

GitHub - TpsReader.

TpsReader for .NET

TpsReader is a simple, read-only .NET library for opening TopSpeed (.TPS)
files and reading their tables and records. TpsReader.Tool supplies the tps
command for schema discovery, filtering, JSON/JSONL output, and CSV export.

Both packages target .NET 8 and support .NET 8 or later. The tool automatically
rolls forward to a newer major runtime when .NET 8 is not installed. Version
0.3.0 is a breaking rename from TpsParser and TpsInspector; no compatibility
namespace or shim package is provided.

Library usage

dotnet add package TpsReader --version 0.3.3

Basic usage

Open a file and iterate through its tables and records:

using TpsReader;

var file = TpsFile.Open(@"C:\data\CUSTOMER.TPS");

foreach (var table in file.Tables)
{
    Console.WriteLine($"{table.Name}: {table.Records.Count} records");

    foreach (var record in table.Records)
    {
        var customerNumber = record.GetInt32("CUS:CUSTNUMBER");
        var company = record.GetString("COMPANY");

        Console.WriteLine($"{customerNumber}: {company}");
    }
}

LINQ filtering and projection

TpsTable.Records is LINQ-ready, so consumers can use normal LINQ without a
package-specific query API:

using TpsReader;

var file = TpsFile.Open(@"C:\data\CUSTOMER.TPS");
var customers = file.GetTable();

var companies = customers.Records
    .Where(record => record.GetString("STATE") == "AZ")
    .Select(record => new
    {
        Number = record.Get<int>("CUSTNUMBER"),
        Company = record.GetString("COMPANY")
    })
    .ToArray();

Use GetTable(string) for a case-insensitive table name or GetTable(int) for
a table number. Parameterless GetTable() succeeds only when the file contains
exactly one table.

Records support an indexer, generic conversion, and familiar typed helpers:

var record = customers.Records[0];

var company = (string?)record["COMPANY"];
var customerNumber = record.Get<int>("CUSTNUMBER");
var phones = record.Get<string[]>("PHONES");

if (record.TryGet<decimal>("BALANCE", out var balance))
{
    Console.WriteLine(balance);
}

GetValue, the indexer, Get<T>, and TryGet<T> resolve ordinary fields,
MEMOs, and BLOBs through the same case-insensitive name rules. Full schema names
and unambiguous short names are accepted. BLOB and other byte-array results are
defensive copies.

Numeric Get<T> conversions are checked. A DECIMAL value converts exactly to
decimal when it is representable; otherwise it remains available losslessly
through GetDecimalString. Arbitrary strings are not parsed as numbers.
Ordinary fixed-width STRING values have trailing NUL and space padding removed.

GROUP values

GROUP fields default to a fixed-width string projection. Only schema-declared
STRING, CSTRING, and PSTRING leaves are placed at their byte offsets; binary
members and gaps become spaces. Internal and trailing spaces are preserved.

string groupText = record.Get<string>("ADDRESS_GROUP")!;
byte[] originalGroupBytes = record.Get<byte[]>("ADDRESS_GROUP")!;

string[] groupArray = record.Get<string[]>("LINES")!;
byte[][] originalArrayBytes = record.Get<byte[][]>("LINES")!;

GROUP width is measured in bytes. Array elements repeat the first element’s
child layout, including nested string leaves and string arrays. Raw byte results
are cloned.

Other inputs and options

Open and TryOpen accept a path, readable Stream, or complete byte[].
Streams are consumed from their current position and are never disposed or
rewound by the library. Input arrays are treated as read-only.

var encrypted = TpsFile.Open(
    @"C:\data\encrypted.tps",
    new TpsOpenOptions { Owner = "owner-password" });

if (!TpsFile.TryOpen(tpsBytes, out var parsed, out var error))
{
    Console.Error.WriteLine(error!.Message);
}

Set IgnoreErrors = true only for intentional partial recovery. A malformed
data page is discarded atomically; no partial record from that page is returned.
StringEncoding controls schema names, string fields, GROUP text, and MEMO text.

Command-line tool

Install the tool package while keeping the short tps command:

dotnet tool install --global TpsReader.Tool --version 0.3.3
tps --help

Each GitHub release also
includes self-contained native AOT archives:

  • tps-v<version>-win-x64.zip contains tps.exe for 64-bit Windows.
  • tps-v<version>-linux-x64.zip contains tps for 64-bit glibc-based Linux.
  • tps-v<version>-osx-arm64.zip contains tps for Apple silicon macOS.

None of these executables require a separate .NET installation.

Build or run it from this repository:

dotnet build TpsReader.sln -c Release
dotnet test TpsReader.sln -c Release
dotnet run --project src\TpsReader.Tool -c Release -- schema C:\data\CUSTOMER.TPS

To create local packages without publishing them:

dotnet pack src\TpsReader -c Release -o artifacts\packages
dotnet pack src\TpsReader.Tool -c Release -o artifacts\packages

Discover and read

Inspect structure before requesting records:

tps inspect C:\data --recursive
tps schema C:\data\CUSTOMER.TPS
tps schema C:\data\CUSTOMER.TPS --table CUS
tps rows C:\data\CUSTOMER.TPS --table CUS --fields CUSTNUMBER,COMPANY --limit 20

schema and JSON row documents retain formatVersion: 1. rows returns at
most 100 records by default; use --limit, --skip, or explicit --all.
JSONL emits one versioned record per line.

CLI filters

The library does not define a query model: application consumers use LINQ. The
tool keeps its dynamic field selection and --where compiler internal.
Repeated CLI predicates are combined with AND.

Value type Operators
Number, DECIMAL, DATE, TIME eq, ne, lt, le, gt, ge, is-null, is-not-null
STRING, CSTRING, PSTRING, GROUP, MEMO eq, ne, contains, starts-with, ends-with, is-null, is-not-null
BLOB is-null, is-not-null
tps rows CUSTOMER.TPS --where STATE eq AZ --where CUSTNUMBER ge 100
tps rows DATA.TPS --where CONTACT_GROUP contains Smith
tps rows DATA.TPS --where GROUP_ARRAY[2] starts-with West

Use @recordNumber as a numeric pseudo-field. Array selectors are one-based.
Text matching is case-insensitive unless --case-sensitive is supplied. GROUP
operators ignore trailing NUL/space padding while preserving internal spacing.
DATE literals use yyyy-MM-dd; TIME accepts HH:mm:ss, HH:mm:ss.f, or
HH:mm:ss.ff. DECIMAL comparisons remain lossless.

JSON and JSONL emit GROUP projections as full fixed-width strings. DECIMAL
values remain strings, dates and times use ISO text, and BLOBs default to length
plus SHA-256 metadata. Add --blob-mode base64 when complete BLOB content is
required.

CSV export

tps export CUSTOMER.TPS --output C:\export
tps export CUSTOMER.TPS --fields CUSTNUMBER,COMPANY --where STATE eq AZ --output C:\export

GROUP cells are always quoted and preserve their full width, including internal
and trailing spaces. Text MEMOs are columns. BLOBs are separate .blob files
referenced by their CSV cells. Writes are atomic and overwrite existing export
files; source TPS files are never modified.

Encrypted and damaged files

Prefer an environment variable over placing an owner value in command history:

$env:TPS_OWNER = 'secret'
tps rows encrypted.tps --owner-env TPS_OWNER --limit 10

Use --ignore-errors only when incomplete recovery is acceptable. Exit codes
are 0 for success, 1 for invalid path/arguments/query, and 2 for parsing or
export failures. Structured commands keep diagnostics on stderr.

Agent skill

The repository includes the portable read-tps-files
Agent Skill, packaged as the tpsreader plugin for Codex and Claude Code and as
@carlosgtrz/tpsreader-agent-skill for Pi.

codex plugin marketplace add CarlosGtrz/TpsReader --sparse .agents/plugins --sparse plugins
codex plugin add tpsreader@tpsreader
claude plugin marketplace add CarlosGtrz/TpsReader --sparse .claude-plugin plugins
claude plugin install tpsreader@tpsreader
pi install npm:@carlosgtrz/tpsreader-agent-skill@1.0.0

See the agent-skill package documentation for
prerequisites, supported platforms, security behavior, examples, and update or
removal commands.

Maintainers can validate the exact release tarball and perform the interactive
npm/2FA publication with
scripts/publish-agent-skill.ps1. Run it
with -WhatIf first to complete every pre-publication check without logging in
or publishing.

.\scripts\publish-agent-skill.ps1 -WhatIf
.\scripts\publish-agent-skill.ps1

Migrating to 0.3.0

  • Replace the TpsParser package, project, assembly, and namespace with
    TpsReader.
  • Replace TpsInspector with TpsReader.Tool; the executable command remains
    tps.
  • Keep existing TpsFile, TpsTable, TpsRecord, and other Tps* class names.
  • Ordinary fixed-width STRING values are now returned without trailing padding.
  • GROUP values now default to fixed-width text rather than raw bytes or JSON hex.
    Request byte[] or byte[][] explicitly when raw GROUP storage is needed.
  • CLI JSON keeps formatVersion: 1 despite the deliberate GROUP row-value
    semantic change.

Attribution and license

This parser adapts logic from
ctrl-alt-dev/tps-parse, copyright
2012-2021 Erik Hooijmeijer, under the Apache License 2.0. See
Apache-2.0.txt.

TPS parsing is based on reverse engineering and may be incomplete. Verify
output before relying on it for critical work.