gortk

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 24, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

README

gortk

CI Go Reference Go Report Card Go Version Release License Dependencies PRs Welcome

A Go-native take on rtk (the Rust "token killer"): it compresses shell-command output before it reaches an LLM context window. Unlike rtk — a standalone Rust binary you shell out to — gortk is a plain Go package meant to be embedded inside an agent runtime.

It keeps the signal an agent needs (failures, errors, file:line), drops the noise (progress bars, "ok" lines), redacts credentials, and is honest about everything it changed. It is lossless by default: a command with no dedicated filter passes through untouched.

Install

go get github.com/mind-build/gortk

Zero dependencies — the core is pure Go stdlib.

Quickstart

reg := gortk.Default().WithRedaction() // built-in filters + secret masking

res := reg.Compress(gortk.Command{
    Name: "go", Args: []string{"test", "./..."},
    Stdout: stdout, Stderr: stderr, ExitCode: code,
})

fmt.Print(res.Text)                 // compact, secret-free output for the model
if !res.Lossless() {
    log.Print(res.Truncation.Note)  // e.g. "kept 3 failing tests, dropped 412 passing"
}

Token savings

These are measured, not estimated: every number below comes from running the bundled fixtures through the default registry. Reproduce them yourself:

go test -run TestSavings -v

Across all bundled fixtures gortk removes ~47% of bytes — and that figure is deliberately conservative, because roughly half the fixtures are failure-preserving correctness cases that gortk does not compress (a failed grep, a git status with changes → 0% by design). On the commands where there is noise to cut, the reduction is much larger:

command in (B) out (B) saved
bundle install 725 59 92%
liquibase 1617 287 82%
kubectl 465 92 80%
poetry install 445 89 80%
rspec 840 203 76%
dotnet build 875 275 69%
xcodebuild 1569 539 66%
rubocop 881 319 64%
ssh 453 167 63%
ping 1088 508 53%
ls 525 259 51%

Fixtures are small by design (they're correctness tests). Real output skews much higher: a green go test ./... with hundreds of passing tests collapses to a single summary line — far past 90%.

Measure your own

Don't take our fixtures' word for it — measure your real traffic. Attach the Stats observer and gortk reports cumulative savings across everything it sees:

stats := &gortk.Stats{}
reg := gortk.Default().WithRedaction().Observe(stats.Observe)
// ... run a session ...
fmt.Println(stats.Report()) // "gortk: 142 cmds, 4.1 MiB -> 210 KiB (95% saved)"

Why not just use rtk?

rtk has no Go binding; it's a Rust binary. Shelling out to it from an agent means bundling and version-managing a foreign binary in every environment, and accepting its opaque, lossy rewriting — fine for a human at a terminal, risky for an agent that might need the one line rtk dropped.

gortk keeps the good idea (per-command output compression) and changes the defaults that matter for an agent.

gortk vs rtk

rtk gortk
Language / runtime Rust binary Go package, embedded in-process
Distribution shell out to an external binary go get, no subprocess
Raw speed faster (compiled Rust) fast enough — see note below
Default for unknown command rewrites only known commands lossless passthrough
Loss visibility inline … (N omitted) markers structured Truncation (agent-readable)
Credential redaction ✓ — masks secrets, including on passthrough
Filter authoring format TOML JSON (machine-generated from rtk's TOML)
Runtime dependencies zero (TOML parser isolated in rtkcompat)
Catalog 100+ commands (source of truth) imported from rtk + extras, re-synced via rtksync
Structured / JSON output hand-written Rust parsers declarative json stage + code filters
Full-output recovery tee Sink + Truncation.FullRef
Savings / discovery rtk gain / rtk discover Stats / Discovery observers

On speed: gortk does not try to beat a compiled Rust binary, and for this workload it doesn't need to. Compression is a few regexes over a few KB of text; the cost that actually matters is the process hop, and being an in-process library avoids the fork/exec + pipe marshalling that shelling out to rtk requires. The bet is dependency-free embedding and agent-safe defaults, not raw throughput.

On TOML vs JSON: this isn't a performance choice — it's about dependencies. rtk authors its filters in TOML; gortk stores the same rules as JSON so the runtime can parse them with the standard library and stay zero-dependency (a TOML parser would be an import). We get rtk's catalog without its dependency by machine-translating TOML → JSON with rtksync (which keeps the TOML parser quarantined in the rtkcompat module). So: TOML to author upstream, JSON to ship.

Two halves: input (run) and output (parse)

gortk splits cleanly into two independent halves that meet only at the Command type. You can use either alone.

INPUT  half:  Invocation ──Runner.Run──▶ Command        (runs; never parses)
                                            │
OUTPUT half:  Command ──Registry.Compress──▶ Result     (parses; never runs)

glue (optional):  Session = Runner + Registry
  • InputRunner.Run(ctx, Invocation) (Command, error). ExecRunner is the built-in os/exec implementation; a host with its own executor (a hardened or sandboxed exec RPC) implements Runner over that. Runners never compress.
  • OutputRegistry.Compress(Command) Result. Pure: it executes nothing, so you can feed it recorded fixtures or output captured by any executor.
  • GlueSession composes the two, and is optional sugar:
cmd, res, err := gortk.DefaultSession().Run(ctx, gortk.Invocation{
    Name: "go", Args: []string{"test", "./..."},
})
// cmd = raw capture, res.Text = compressed view, res.Truncation = what was dropped

For the common in-process case there are package-level shortcuts over a shared default session (filters compiled once): gortk.Run(ctx, inv) and gortk.RunStream(ctx, inv, onLine).

The point of the split: a host can keep its own sanctioned execution and use only the output half; standalone callers (and the CLI) use both. Neither half depends on the other.

Streaming (long-running commands)

For commands where you want output live (build/test progress, early signals), the input half also has a streaming form. It's an optional capability — StreamRunner, which ExecRunner implements — that emits each line as it arrives and still returns the full Command at the end:

cmd, res, err := gortk.DefaultSession().RunStream(ctx, inv, func(ev gortk.StreamEvent) {
    fmt.Printf("%s| %s\n", ev.Stream, ev.Line) // live, line by line
})
// res = compressed view, computed once at the end from the full cmd

Compression stays whole-output (match_output, json, head/tail all need the complete result), so the model-facing compression still happens once when the command finishes — streaming is purely about observing progress meanwhile. Callbacks are serialized (stdout/stderr never overlap), so no locking is needed. Session.RunStream degrades to a batch Run if the underlying runner can't stream, so callers use one code path. CLI: gortk run --stream -- <cmd>.

Grounded in rtk's actual implementation

I read rtk's source (not just its README) before building this. Two things stood out and shaped the design:

  1. rtk is itself schema-driven. Its compression rules live in TOML (.rtk/filters.toml, engine in src/core/toml_filter.rs) — match_command, strip_lines_matching/keep_lines_matching, head_lines/tail_lines, max_lines, on_empty, match_output, replace. gortk's Spec is the same idea in JSON, so rules are editable/loadable without a recompile.
  2. rtk splits structured parsers from the declarative engine. It has ~60 hand-written Rust parsers in src/cmds/** for rich formats (go test, pytest, golangci-lint, cargo…) and the TOML engine for the long tail. gortk uses the exact same split: code Filters in filters.go, declarative Specs for the rest.

For how the two compare overall, see gortk vs rtk above.

Parity with rtk's runtime features

The ideas rtk leans on at runtime have native, agent-shaped equivalents here:

rtk feature gortk equivalent
tee (save full output of failed commands) Registry.WithSink + FileSink; the handle rides on Truncation.FullRef (see Full-output recovery)
rtk gain (token-savings report) Result.SavedBytes/SavedFraction + the Stats observer
rtk discover (find missed optimizations) the Discovery observer — records every command that fell through to passthrough
grouping (files by dir, errors by type) the group spec stage
-u ultra-compact Registry.WithCompact / CLI -u

And one thing rtk doesn't do: credential redactionRegistry.WithRedaction() masks secrets (incl. on passthrough) before output reaches the model. See Secret redaction.

Catalog

94 filters, organized by ecosystem in specs/<eco>.json (plus the go-test structured code filter). The bulk are imported from rtk's TOML catalog and kept in sync from upstream (see Keeping the catalog in sync); the rest are hand-written extras.

Ecosystem Covered (selection)
Go go build/vet/run/mod/test, golangci-lint
Python pytest, ruff, mypy, ty, basedpyright, pip/poetry/uv
JS/TS tsc, eslint, prettier, npm/pnpm, vitest, biome, oxlint, turbo, nx, next
Ruby rspec, rubocop, rake, bundle install
JVM mvn, gradle, spring-boot
Cloud/Infra aws, kubectl, psql, gcloud, helm, skopeo, terraform/tofu, ansible, docker build
Build make, cargo, gcc, just, task, dotnet, swift-build, xcodebuild
System grep, find, ls, tree, ps, df, du, ssh, rsync, ping, systemctl, …
Git git status/push/pull, gh, jj
Lint/misc shellcheck, hadolint, yamllint, markdownlint, curl, and more

Add a tool by appending a Spec to the relevant specs/*.json — no code.

How the catalog was built
  • rtk's TOML filters (src/filters/*.toml) are imported deterministically via the rtk2json converter (see below) — a faithful, mechanical translation.
  • rtk's structured Rust parsers (src/cmds/**, e.g. aws, rspec, psql) have no TOML, so they were ported to line/JSON Specs and each ships fixtures in testdata/structured_fixtures.json, run by TestStructuredFixtures.
Zero-dependency core; TOML import is a separate module

gortk core (this module) is pure stdlib — zero external dependencies, so embedding it pulls nothing extra. The rtk-TOML importer lives in a sibling module, rtkcompat/, which is the only thing that depends on a TOML parser.

To import rtk (or community) .toml filters into JSON specs, run the converter from the rtkcompat module:

cd rtkcompat
go run ./cmd/rtk2json [--tests] [--exclude a,b] path/to/rtk/src/filters/*.toml > ../specs/rtk.json

Field mapping (match_commandcommand_regex, strip_lines_matchingdrop_regexps, keep_lines_matching→whitelist keep_regexps, on_emptyempty_text, …) is in rtkcompat. Re-run it to pull upstream updates. The runtime never touches it — specs/*.json is JSON-only.

Go templates for JSON output

json.item_template / summary_template auto-detect syntax: plain {a.b} for the common case, or a full Go text/template when the string contains {{. The template form handles nested arrays (e.g. rubocop's files[].offenses[]) via range, conditionals, and printf — all stdlib, still zero-dep:

"item_template": "{{$p := .path}}{{range .offenses}}{{$p}}:{{.location.start_line}} [{{.cop_name}}] {{.message}}\n{{end}}"

Schema (the Spec type)

A Spec is JSON and runs as an ordered pipeline (mirrors rtk's stages):

  1. match_output — whole-blob regex → one-line message, with an unless guard so errors are never hidden behind an "ok" (rtk's highest-leverage stage).
  2. json — flatten a JSON array to one templated line per element.
  3. log — parse a log stream into levelled Records (see below).
  4. linesstrip_ansi, trim_space, drop_blank, dedup_adjacent, drop_prefixes, drop_regexps, keep_regexps (whitelist).
  5. group — collapse surviving lines that share a key_regex capture into one {key} ({count}) summary line (rtk's grouping; e.g. files by directory).
  6. limitmax_lines head/tail cap.
  7. empty_text — message when nothing survives.

Example (git push → one line unless it failed):

{
  "name": "git-push",
  "match": { "command": "git", "subcommands": ["push"] },
  "match_output": [
    { "pattern": "(?m)^To .+", "unless": "(?i)error|rejected|fatal", "message": "git push: ok" }
  ]
}

Load and register at runtime:

specs, _ := gortk.LoadSpecs(jsonBytes)
reg := gortk.Default()
for _, s := range specs { _ = reg.RegisterSpec(s) }
Log parsing → structured Records

The log stage parses a log stream: a named-capture regex extracts fields per line, level_map normalizes severities, demote_patterns drop routine noise to debug, min_level filters, and template renders each kept line. It produces structured Records ({Level, Fields, Text}) on the Result, not just compressed text — so a caller can route by level or read fields directly.

It's deliberately parse → route → render, not a query language: no expressions, no aggregation. That's where the "swiss-army" line is drawn.

The compiled form, LogParser, parses one line at a time, so the same spec serves batch compression and streaming consumers (e.g. postgres' log writer routing each line to a logger at its level — no buffering):

p, _ := gortk.LogSpec{
    LineRegex: `^\S+ \S+ \S+ \[(?P<pid>\d+)\] (?P<level>\w+):\s*(?P<msg>.*)$`,
    LevelMap:  map[string]string{"LOG": "info", "FATAL": "fatal", "WARNING": "warn"},
    DemotePatterns: []string{`^checkpoint (starting|complete)`},
}.Compile()

for line := range stream {              // streaming, line by line
    rec := p.Parse(line)                // -> {Level, Fields{pid,msg,...}, Text}
    logger.At(rec.Level, rec.Fields["msg"])
}

Design

Three rules, in priority order:

  1. Lossless by default. A command with no dedicated filter is passed through untouched (only size-bounded). Adding gortk can never silently destroy signal.
  2. Failure-preserving. Filters drop known noise (ok lines, === RUN chatter, git's human hints) but never failures, errors, or file:line locations.
  3. Honest about loss. Anything dropped or truncated is recorded in Result.Truncation — a compact "this view is partial" signal a host can surface to its agent or UI.

Runtime features

These are the runtime conveniences rtk ships, re-cast for an embedded agent. All are opt-in and the core stays zero-dependency.

Full-output recovery (tee)

Recording loss is only half the promise — an agent also needs to recover the line a filter dropped without re-running the command. Attach a Sink and every lossy Result carries a handle to its full, uncompressed output:

reg := gortk.Default().WithSink(gortk.FileSink{Dir: teeDir})

res := reg.Compress(cmd)
if res.Truncation.FullRef != "" {
    // hand the agent a "read full output" affordance pointing at this path
    full, _ := os.ReadFile(res.Truncation.FullRef)
}

FileSink writes to disk (rtk's tee dir); implement Sink yourself to stash full output anywhere (blob store, in-memory LRU, RPC). The sink is consulted only when something was actually dropped — a lossless result holds nothing.

Savings (gain) and discovery

Every Result from Compress records InputBytes/OutputBytes, so savings is computable per command (res.SavedFraction()). For cumulative reporting and to learn which commands you should write a filter for next, register observers:

stats := &gortk.Stats{}       // rtk's "gain"
disc  := &gortk.Discovery{}   // rtk's "discover" (records passthrough commands)
reg := gortk.Default().Observe(stats.Observe).Observe(disc.Observe)

// ... run a session ...
fmt.Println(stats.Report())          // "gortk: 142 cmds, 4.1 MiB -> 210 KiB (95% saved)"
for _, e := range disc.Top(10) {     // commands with no dedicated filter, by frequency
    fmt.Printf("%4d  %s\n", e.Count, e.Command)
}

Discovery is the data-driven answer to "is the catalog the right 94?" — it tells you exactly what your agents run that still falls through to passthrough.

Grouping

The group stage collapses many similar lines into one summary line per key — rtk's grouping strategy. The key_regex's first capture is the group key; non-matching lines pass through untouched, so a header above a list is kept:

{
  "name": "find",
  "match": { "command": "find" },
  "group": { "key_regex": "^(.*)/[^/]+$", "line": "{key}/ ({count} files)", "examples": 0 }
}

examples: N keeps up to N indented sample lines under each header.

Ultra-compact

Registry.WithCompact() (CLI -u) adds a final whitespace-collapsing pass: runs of blank lines collapse to one, leading/trailing blanks are trimmed. It only ever removes whitespace, so it's safe to leave on; removed blanks are still counted in Truncation.

Secret redaction (beyond rtk)

rtk compresses output; it doesn't sanitize it. gortk adds a registry-wide redaction pass that masks credentials before they enter the model's context — and it runs on every result, including lossless passthrough, because the commands most likely to leak (env, printenv, cat .env, aws configure list) are exactly the ones with no dedicated filter:

reg := gortk.Default().WithRedaction()

res := reg.Compress(gortk.Command{Name: "env", Stdout: envDump})
// AWS_SECRET_ACCESS_KEY=wJalr… -> AWS_SECRET_ACCESS_KEY=[REDACTED]
// res.Truncation.Masked counts how many spans were masked

The default set is high-precision patterns — cloud keys (AWS/GCP), GitHub and Slack tokens, JWTs, PEM private keys, Bearer tokens, key=value / "key": "value" secrets, and URL-embedded credentials — chosen to almost never fire on ordinary build/test output. Masking replaces a span with a marker rather than dropping a line, so it does not flip Lossless(); it's reported via Truncation.Masked.

For a catch-all, opt into entropy scanning. It's more aggressive (it can mis-fire on opaque-but-harmless tokens), so it's off by default and allowlists common identifiers (UUIDs, git SHAs, hex digests):

reg, _ := gortk.Default().WithRedactionOptions(gortk.RedactOptions{
    Entropy: true,             // also mask high-entropy blobs
    Extra:   []string{`xoxb-\w+`}, // your own patterns
})

Note: a configured Sink (--tee) saves the full, unredacted output locally for recovery — redaction protects the model context, not your disk.

Normalization

Registry.WithNormalize() (CLI --normalize) collapses volatile, high-cardinality tokens — UUIDs, ISO timestamps, IPs, hex digests — to stable markers (<uuid>, <ts>, <ip>, <hash>). This cuts tokens and lets dedup_adjacent collapse log lines that differ only by a request id. It changes content (you lose the specific id), so it's opt-in and counted in Truncation.Masked.

Usage

reg := gortk.Default() // go test, golangci-lint, git status + passthrough

res := reg.Compress(gortk.Command{
    Name:     "go",
    Args:     []string{"test", "-json", "./..."},
    Stdout:   stdout,
    Stderr:   stderr,
    ExitCode: code,
})

fmt.Print(res.Text)        // compact output for the model
if !res.Lossless() {
    log.Print(res.Truncation.Note) // e.g. "kept 3 failing tests, dropped 412 passing"
}

Add a project-specific filter:

reg := gortk.Default().Register(MyPytestFilter{})

A filter is anything implementing:

type Filter interface {
    Name() string
    Match(name string, args []string) bool
    Apply(cmd Command) Result
}

Where this fits

If your runtime already has a structured runner for a tool — one that parses go test -json, pytest JUnit XML, cargo, jest, etc. into a typed result and keeps only failed cases — keep using it. gortk is not a replacement for that.

gortk is for the generic command surface an agent hits that has no dedicated runner: arbitrary git, docker, make, lint, and one-off shell commands that otherwise dump raw bytes straight into the context window. gortk slots in right there — between your executor returning raw bytes and those bytes entering the model's context — giving that long tail the same "keep signal, drop noise, redact secrets, record loss" treatment your first-class runners already get, without each agent reinventing it.

Integrating into a host runtime

Short version: use the library, not a CLI. A host that runs commands in-process already holds the raw output, so compressing is one call:

var compressor = gortk.Default().WithRedaction() // build once, reuse (concurrency-safe)

view := compressor.Compress(gortk.CommandFromArgs(
    append([]string{req.Command}, req.Args...),
    stdout, stderr, exitCode,
))
// view.Text -> model context; view.Truncation -> what was dropped / masked

The natural seam is wherever command output is about to enter the model's context. See INTEGRATION.md for copy-pasteable adapters (argv and shell-line forms), the "do we need a CLI?" decision matrix, and the recommended keep-raw-add-a-field approach.

CLI (optional dev tool)

cmd/gortk is for iterating on filters from a terminal — not the runtime path:

go build -o gortk ./cmd/gortk

go test ./... 2>&1 | gortk filter go test   # compress piped output
gortk run -- go test ./...                  # run a command and compress it
gortk run --tee /tmp/gortk -- go test ./... # keep full output of lossy results
gortk -u run -- go test ./...               # ultra-compact (collapse blanks)
gortk --redact filter env                   # mask credentials in piped output
gortk --normalize filter docker ps          # collapse UUIDs/timestamps/hashes
gortk specs                                 # list active filters
gortk --specs my.json filter docker build   # try extra specs without a rebuild

run/filter print a one-line gain report to stderr (N -> M bytes (P% saved)), the recovery path when --tee is set, masked-span counts when redacting, and a hint when no filter matched.

Keeping the catalog in sync with rtk

rtk releases roughly weekly, so specs/rtk.json is generated from upstream — never hand-edited. Regenerate it with the Go importer, rtksync, which clones rtk and converts its whole TOML catalog deterministically (-check mode just detects drift, which CI runs on every PR):

go generate ./...                              # re-import via rtksync
cd rtkcompat && go run ./cmd/rtksync           # equivalent, run directly
cd rtkcompat && go run ./cmd/rtksync -check    # fail if specs/rtk.json is stale

Pin a release with -ref v0.42.4, use a local checkout with -dir path/to/rtk, or drop filters with -exclude name1,name2. The runtime never touches rtk or TOML — specs/*.json is plain JSON, loaded by the zero-dependency core.

Status

Reference filters: go test (code), golangci-lint, git push, git status (specs), plus lossless passthrough, and 94 imported/extra filters. Adapters for argv and shell-line commands. Runtime features: secret redaction (Redactor), normalization, full-output recovery (Sink), savings (Stats), discovery (Discovery), grouping, ultra-compact. Re-sync rtk's catalog with go generate ./...; run the suite with go test ./....

License

Apache 2.0.

Documentation

Overview

Package gortk compresses shell-command output before it reaches an LLM context window. It is a Go-native take on rtk (the Rust "token killer"), built for embedding inside an agent runtime rather than shelling out to an external binary.

Design principles, in order of priority:

  1. Lossless-by-default. The generic path only bounds size; it never drops signal. A command without a dedicated filter is always safe to pass through gortk.
  2. Failure-preserving. Per-command filters drop *known noise* (progress bars, "ok" lines, dependency download chatter) but never the lines an agent needs to act on (failures, errors, file:line locations).
  3. Honest about loss. Whenever a filter drops or truncates anything, it records it in Truncation so the caller — and the agent — knows the view is partial.

The unit of work is a Command (what ran + what it produced) and the result is a Result (a compressed view + truncation metadata).

Index

Constants

View Source
const DefaultMaxBytes = 32 * 1024

DefaultMaxBytes bounds the passthrough (and post-filter) output size. It is the last line of defence against a runaway command flooding the context. The value (32 KiB) is a deliberately small per-command cap — gortk output is meant to already be compact, unlike a raw shell capture.

View Source
const DefaultMaxCaptureBytes = 2 << 20 // 2 MiB per stream

DefaultMaxCaptureBytes bounds each captured stream in ExecRunner. It matches a common 2 MiB host-executor cap so behavior is consistent if you swap runners.

Variables

This section is empty.

Functions

func ParseCommandLine

func ParseCommandLine(line string) (name string, args []string)

ParseCommandLine splits a shell command line into a program name and args, honoring single quotes, double quotes, and backslash escapes. It stops at the first shell control operator (| & ; < > ( )) so only the leading command is returned. It is intentionally small — enough to route to a filter, not a full POSIX shell parser.

func Run

func Run(ctx context.Context, inv Invocation) (Command, Result, error)

Run is a convenience shortcut: run inv with the default in-process session (ExecRunner + the built-in filters) and return the raw capture plus the compressed view. Equivalent to gortk.DefaultSession().Run, but reuses one shared session instead of recompiling filters on each call.

func RunStream

func RunStream(ctx context.Context, inv Invocation, onLine StreamFunc) (Command, Result, error)

RunStream is the streaming convenience shortcut, mirroring the package-level Run: it streams lines from the default in-process session and compresses the final output. Reuses the shared default session.

func StripANSI

func StripANSI(s string) string

StripANSI removes ALL terminal control sequences from s, including SGR color codes. Use for LLM/text contexts where colors are noise. The one canonical sanitizer — other packages share it rather than hand-rolling their own.

func StripControl

func StripControl(s string) string

StripControl removes cursor/OSC/charset/DCS/2-char control sequences and bare CR/BEL, but KEEPS SGR color codes (\x1b[…m). Use when forwarding logs to a human terminal: control sequences garble layout, but colors are useful.

Types

type Command

type Command struct {
	Name     string   // argv[0], e.g. "go", "git", "golangci-lint"
	Args     []string // argv[1:], e.g. ["test", "./..."]
	Stdout   []byte
	Stderr   []byte
	ExitCode int
}

Command is the input to compression: a finished command invocation and the bytes it produced. Stdout/Stderr are kept separate because most filters care about the distinction (e.g. test runners write results to stdout, diagnostics to stderr).

func CommandFromArgs

func CommandFromArgs(argv []string, stdout, stderr []byte, exitCode int) Command

CommandFromArgs builds a Command from an argv slice (argv[0] is the program). Use this when you ran the command in exec/argv form, e.g. an exec-style request carrying a command name and its arguments.

func CommandFromLine

func CommandFromLine(line string, stdout, stderr []byte, exitCode int) Command

CommandFromLine builds a Command from a shell command line (the `sh -c "…"` form). The line is tokenized with shell-like quoting so `git commit -m "a b"` yields Name=git, Args=[commit -m "a b"]. Only the first simple command is inspected — everything up to the first pipe/redirect/operator — which is enough to pick a filter.

func (Command) Sub

func (c Command) Sub() string

Sub reports the first positional argument (the subcommand), e.g. "test" for `go test ./...`. Flags are skipped. Returns "" if there is none.

type Discovery

type Discovery struct {
	// contains filtered or unexported fields
}

Discovery records the commands that fell through to lossless passthrough — i.e. those gortk has no dedicated filter for. It is the data-driven answer to "which filter should I write next?" (rtk's "discover"): rather than porting rtk's whole catalog blind, you learn exactly what your agents actually run that isn't yet compressed. Wire it as an observer:

disc := &gortk.Discovery{}
reg := gortk.Default().Observe(disc.Observe)
...
for _, e := range disc.Top(10) {
    fmt.Printf("%4d  %s\n", e.Count, e.Command)
}

The zero value is ready to use and safe for concurrent use.

func (*Discovery) Observe

func (d *Discovery) Observe(cmd Command, res Result)

Observe records cmd when its result came from passthrough (no filter matched). Its signature matches Registry.Observe, so it can be passed directly. Results that a real filter produced are ignored — those are already covered.

func (*Discovery) Top

func (d *Discovery) Top(n int) []DiscoveryEntry

Top returns the n most-seen uncompressed commands, highest count first (ties broken by total bytes, then name). n <= 0 returns all of them.

type DiscoveryEntry

type DiscoveryEntry struct {
	Command string // e.g. "git rebase"
	Count   int    // times seen with no matching filter
	Bytes   int64  // total uncompressed bytes these passthroughs carried
}

DiscoveryEntry is one uncompressed command family and how often it was seen.

type ExecRunner

type ExecRunner struct {
	// MaxCaptureBytes bounds each of stdout/stderr. 0 uses DefaultMaxCaptureBytes.
	MaxCaptureBytes int
}

ExecRunner is the default Runner: it runs commands with os/exec and captures stdout/stderr into bounded buffers. Hosts with a hardened executor (process groups, sandboxing, RPC) should implement Runner over that instead.

func (ExecRunner) Run

func (r ExecRunner) Run(ctx context.Context, inv Invocation) (Command, error)

Run executes inv and returns the captured Command. A non-zero exit status is reported in Command.ExitCode and is NOT a Go error. A Go error is returned only when the process could not run to completion (failed to start, context cancelled, timed out) — the partial Command is still returned alongside it.

func (ExecRunner) RunStream

func (r ExecRunner) RunStream(ctx context.Context, inv Invocation, onLine StreamFunc) (Command, error)

RunStream runs inv, invoking onLine for each line of stdout/stderr as it is produced, and returns the full captured Command when the process exits. Exit semantics match Run: a non-zero exit is reported in Command.ExitCode, not as an error; a Go error means the process could not run to completion.

type FileSink

type FileSink struct {
	// Dir is where full-output files are written. Created on first use. Empty =
	// os.TempDir()/gortk-tee.
	Dir string
}

FileSink persists full command output to files under Dir, returning each file's path as the recovery handle. It is the disk-backed Sink (rtk's tee dir). Use it via Registry.WithSink:

reg := gortk.Default().WithSink(gortk.FileSink{Dir: teeDir})

The zero value writes to os.TempDir()/gortk-tee. FileSink is safe for concurrent use; filenames are unique per call.

func (FileSink) Save

func (s FileSink) Save(cmd Command, _ Result) (string, error)

Save writes cmd's full output (stderr then stdout, the same order as passthrough) to a uniquely named file and returns its path.

type Filter

type Filter interface {
	// Name identifies the filter in Result.Filter and in logs.
	Name() string

	// Match reports whether this filter handles the given command. It is given
	// the program name and args so it can key off subcommands
	// (e.g. only `go test`, not every `go` invocation).
	Match(name string, args []string) bool

	// Apply produces the compressed Result. It is only called when Match
	// returned true.
	Apply(cmd Command) Result
}

Filter compresses the output of one family of commands.

Implementations should be pure (no I/O, no global state) so they are trivial to unit-test against captured fixtures.

type GoTest

type GoTest struct{}

GoTest compresses `go test` output. It prefers the structured `-json` stream (the same structured source a Go test runner consumes) and falls back to scanning plain text. It keeps failures, build errors, and the final summary; it drops per-package "ok" lines and "=== RUN"/"--- PASS" chatter, which dominate the byte count on a green run.

func (GoTest) Apply

func (g GoTest) Apply(cmd Command) Result

func (GoTest) Match

func (GoTest) Match(name string, args []string) bool

func (GoTest) Name

func (GoTest) Name() string

type GroupSpec

type GroupSpec struct {
	KeyRegex string `json:"key_regex"`          // capture group 1 = the grouping key
	Line     string `json:"line,omitempty"`     // header template; {key} and {count}. Default "{key} ({count})"
	Examples int    `json:"examples,omitempty"` // keep up to N example lines under each header (indented). 0 = none
}

GroupSpec collapses lines that share a key into one summary line per key — rtk's "grouping" strategy (files by directory, errors by type). It runs after the line stage's drop/keep rules, on whatever survives. A line whose KeyRegex captures group 1 is grouped under that key; a line that doesn't match passes through untouched (so a header or error above a list is preserved).

Example — collapse `find` output by directory:

"group": { "key_regex": "^(.*)/[^/]+$", "line": "{key}/ ({count} files)", "examples": 0 }

type Invocation

type Invocation struct {
	Name    string
	Args    []string
	Dir     string        // working directory; "" = current
	Env     []string      // extra KEY=VALUE entries, appended to the process env
	Stdin   []byte        // optional stdin
	Timeout time.Duration // 0 = no timeout
}

Invocation describes a command to run — the pure input description, with no notion of compression. A Runner turns it into a Command (raw output).

type JSONSpec

type JSONSpec struct {
	ArrayField      string `json:"array_field"`   // top-level field holding the array
	ItemTemplate    string `json:"item_template"` // e.g. "{Pos.Filename}:{Pos.Line} {Text}"
	SummaryTemplate string `json:"summary_template,omitempty"`
}

JSONSpec flattens a JSON array into one compact line per element.

Templates support two syntaxes, auto-detected: if a template contains "{{" it is a full Go text/template (powerful — conditionals, range, printf), executed against the element (a map[string]any); otherwise it uses the lightweight {dotted.path} placeholder form. SummaryTemplate additionally gets {count} / {{.count}}.

type LimitSpec

type LimitSpec struct {
	MaxLines int    `json:"max_lines,omitempty"`
	Keep     string `json:"keep,omitempty"` // "tail"(default)|"head"
}

LimitSpec caps the post-transform size and records the loss.

type LineSpec

type LineSpec struct {
	Source        string   `json:"source,omitempty"`     // "stdout"(default)|"stderr"|"both"
	StripANSI     bool     `json:"strip_ansi,omitempty"` // remove ANSI color/escape codes first
	TrimSpace     bool     `json:"trim_space,omitempty"`
	DropBlank     bool     `json:"drop_blank,omitempty"`
	DedupAdjacent bool     `json:"dedup_adjacent,omitempty"`
	DropPrefixes  []string `json:"drop_prefixes,omitempty"`
	DropRegexps   []string `json:"drop_regexps,omitempty"`
	KeepRegexps   []string `json:"keep_regexps,omitempty"` // whitelist: keep ONLY matching lines (drop rules still apply on top)

	// TruncateLinesAt caps each surviving line to N runes (adds "…"). 0 = off.
	// Equivalent to rtk's truncate_lines_at; good for tools that emit very long
	// single lines (minified diffs, embedded data).
	TruncateLinesAt int `json:"truncate_lines_at,omitempty"`
}

LineSpec is a line-oriented transform: drop known-noise lines, keep the rest. Keep rules win over drop rules, so you can drop broadly and rescue specifics.

type LogParser

type LogParser struct {
	// contains filtered or unexported fields
}

LogParser is a compiled LogSpec. It is read-only after construction (safe for concurrent use) and parses one line at a time, so it serves both batch compression and streaming consumers.

func (*LogParser) Below

func (p *LogParser) Below(rec Record) bool

Below reports whether a record falls under the spec's MinLevel and so should be dropped in batch mode. Streaming consumers can ignore this and let their own logger threshold filter.

func (*LogParser) Parse

func (p *LogParser) Parse(line string) Record

Parse turns one raw line into a Record: named captures become fields, the level is mapped/demoted, and the template renders the text. A line that doesn't match line_regex becomes {level: default, fields:{msg: line}} — so nothing is silently dropped.

type LogSpec

type LogSpec struct {
	Source         string            `json:"source,omitempty"`          // stdout|stderr|both (default both — logs usually go to stderr)
	LineRegex      string            `json:"line_regex"`                // named groups become fields; group "msg" is the message
	LevelField     string            `json:"level_field,omitempty"`     // capture holding the raw severity (default "level")
	LevelMap       map[string]string `json:"level_map,omitempty"`       // raw token -> canonical level (debug|info|warn|error|fatal)
	DefaultLevel   string            `json:"default_level,omitempty"`   // unmatched lines / unknown severities (default "info")
	DemotePatterns []string          `json:"demote_patterns,omitempty"` // message matches one -> level becomes "debug"
	MinLevel       string            `json:"min_level,omitempty"`       // drop records below this (default: keep all)
	Template       string            `json:"template,omitempty"`        // per-record render; {field} or Go template; default "{msg}"
}

LogSpec is the declarative log transform.

func (LogSpec) Compile

func (s LogSpec) Compile() (*LogParser, error)

Compile builds a LogParser, precompiling the regex, demote patterns, and template.

type MatchSpec

type MatchSpec struct {
	Command     string   `json:"command,omitempty"`      // base program name, e.g. "git"
	Subcommands []string `json:"subcommands,omitempty"`  // first positional must be one of these
	ArgsContain []string `json:"args_contain,omitempty"` // every string must appear in args

	// CommandRegex matches against the reconstructed command line
	// "<base-name> <arg> <arg> …" (e.g. "uv sync --frozen"). When set, it is the
	// sole match condition. Equivalent to rtk's match_command.
	CommandRegex string `json:"command_regex,omitempty"`
}

MatchSpec decides which commands a Spec applies to. Use either the structured fields (Command + Subcommands/ArgsContain) or CommandRegex — the regex form mirrors rtk's match_command and is the simplest way to port its filters.

type OutputRule

type OutputRule struct {
	Pattern string `json:"pattern"`          // regex against the full output blob
	Unless  string `json:"unless,omitempty"` // if this also matches, skip the rule
	Message string `json:"message"`          // emitted when Pattern matches and Unless doesn't
}

OutputRule is one whole-blob collapse rule for Spec.MatchOutput.

type Record

type Record struct {
	// Level is the canonical severity: debug|info|warn|error|fatal (or "" when
	// the line carries no level).
	Level string
	// Fields are the named captures / parsed values for the line. Always
	// includes "msg" (the message) and "level".
	Fields map[string]any
	// Text is the rendered line (per the spec's template).
	Text string
}

Record is one parsed line of structured output: a canonical severity level, the named fields extracted from it, and the rendered text. Produced by log specs and reusable by streaming consumers (see LogParser).

type RedactOptions

type RedactOptions struct {
	// Entropy turns on the catch-all high-entropy token scanner. It is more
	// aggressive and can mis-fire on opaque-but-harmless tokens, so it is off by
	// default; the allowlist (UUIDs, hex hashes) keeps common identifiers safe.
	Entropy bool
	// MinEntropyLen is the shortest token the entropy scanner considers. 0 -> 24.
	MinEntropyLen int
	// EntropyThreshold is the minimum Shannon entropy (bits/char) to redact. 0 -> 4.0.
	EntropyThreshold float64
	// Extra are additional regexes; each whole match is replaced with [REDACTED].
	Extra []string
}

RedactOptions tunes the redactor. The zero value (used by WithRedaction) is the high-precision pattern set with no entropy scanning.

type Redactor

type Redactor struct {
	// contains filtered or unexported fields
}

Redactor masks credentials in text. Build one with WithRedaction / WithRedactionOptions; it is immutable and safe for concurrent use.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry holds an ordered set of filters and applies the first one that matches. The zero value is not usable; build one with New.

func Default

func Default() *Registry

Default returns a Registry wired with the built-in filters: the hand-written structured parsers (go test) plus the embedded declarative Specs (golangci-lint, git status). This is the intended entry point for embedding gortk in an agent: keep one Default registry and call Compress on every command's output.

It panics only if the embedded defaults are malformed, which is a build-time guarantee covered by tests.

func New

func New(filters ...Filter) *Registry

New builds a Registry with the given filters (first match wins) and the default size bound.

func (*Registry) Compress

func (r *Registry) Compress(cmd Command) Result

Compress runs the first matching filter, then bounds the result size. If no filter matches, it returns a lossless passthrough of stdout+stderr (still size-bounded). Compress never errors and never panics on a well-formed Command — a command with no special handling is simply passed through.

After producing the view it records the savings (InputBytes/OutputBytes), persists the full output through any configured Sink when the result is lossy (Truncation.FullRef), and fans the (cmd, result) pair out to observers.

func (*Registry) Observe

func (r *Registry) Observe(fn func(Command, Result)) *Registry

Observe registers a callback invoked with (cmd, result) after every Compress, for metrics and discovery. Multiple observers run in registration order. Observers must not mutate their arguments. Returns the registry for chaining. See Stats (savings) and Discovery (unmatched commands) for ready-made ones.

func (*Registry) Register

func (r *Registry) Register(f Filter) *Registry

Register appends a filter, giving it lowest priority. Use this to add project-specific filters on top of Default().

func (*Registry) RegisterSpec

func (r *Registry) RegisterSpec(s Spec) error

RegisterSpec compiles a Spec and appends it as a lowest-priority filter.

func (*Registry) WithCompact

func (r *Registry) WithCompact() *Registry

WithCompact returns a copy of the registry that applies a final whitespace-collapsing pass to every result (rtk's "-u" ultra-compact spirit): runs of blank lines collapse to one and leading/trailing blanks are trimmed. It only ever removes whitespace, so it is safe to leave on; dropped blank lines are still recorded in Truncation.

func (*Registry) WithMaxBytes

func (r *Registry) WithMaxBytes(n int) *Registry

WithMaxBytes returns a copy of the registry with a different size bound.

func (*Registry) WithNormalize

func (r *Registry) WithNormalize() *Registry

WithNormalize returns a copy of the registry that collapses volatile high-cardinality tokens (UUIDs, ISO timestamps, IPs, hex digests) to stable markers (<uuid>, <ts>, <ip>, <hash>). This cuts tokens and lets dedup collapse otherwise-unique lines. It changes content (loses the specific identifier), so it is opt-in; substitutions are counted in Truncation.Masked.

func (*Registry) WithRedaction

func (r *Registry) WithRedaction() *Registry

WithRedaction returns a copy of the registry that masks credentials in every result — including lossless passthrough, since unfiltered commands (env, printenv, cat .env) are the likeliest to leak. Uses the high-precision default pattern set (cloud keys, tokens, JWTs, PEM private keys, key=value secrets, URL credentials). Strongly recommended for any output bound for a model. Masked spans are counted in Truncation.Masked.

func (*Registry) WithRedactionOptions

func (r *Registry) WithRedactionOptions(opts RedactOptions) (*Registry, error)

WithRedactionOptions is WithRedaction with control over entropy scanning and extra patterns. It returns an error only if an Extra regex fails to compile.

func (*Registry) WithSink

func (r *Registry) WithSink(s Sink) *Registry

WithSink returns a copy of the registry that persists the full output of every lossy result through s, attaching a recovery handle to Truncation.FullRef. Pass nil to disable. Off by default (gortk holds nothing it wasn't asked to).

type Result

type Result struct {
	// Text is the compressed output to hand to the model.
	Text string

	// Filter is the name of the filter that produced this Result, or "passthrough"
	// when no dedicated filter matched.
	Filter string

	// Truncation records what (if anything) was dropped. The zero value means
	// nothing was lost — the Text is a complete, faithful view.
	Truncation Truncation

	// Records carries structured output when a structured stage (a log spec)
	// produced it: one entry per surviving line, with its level and parsed
	// fields. nil for text-only filters. This is the "structured data out" path
	// — a caller can route records to a logger or consume fields directly
	// instead of (or alongside) reading Text.
	Records []Record

	// InputBytes is the size of the raw command output (stdout+stderr) before
	// compression; OutputBytes is len(Text) after it. Both are set by
	// Registry.Compress so callers can report savings (see SavedBytes /
	// SavedFraction and the Stats aggregator). Zero on a hand-built Result that
	// never went through a Registry.
	InputBytes  int
	OutputBytes int
}

Result is the compressed view of a Command's output plus a record of what was lost producing it.

func (Result) Lossless

func (r Result) Lossless() bool

Lossless reports whether the Result preserved everything (nothing dropped or truncated).

func (Result) SavedBytes

func (r Result) SavedBytes() int

SavedBytes reports how many bytes compression removed (InputBytes-OutputBytes, never negative). Meaningful only on a Result returned by Registry.Compress.

func (Result) SavedFraction

func (r Result) SavedFraction() float64

SavedFraction reports the fraction of bytes removed, in [0,1]. Returns 0 when there was no input. This is gortk's answer to rtk's "gain" metric.

type Runner

type Runner interface {
	Run(ctx context.Context, inv Invocation) (Command, error)
}

Runner executes an Invocation and returns the captured output as a Command.

A Runner MUST NOT compress or interpret output — that is the Registry's job. This is the extension point a host implements over its own executor (a hardened or sandboxed exec RPC, say); ExecRunner is the standalone os/exec implementation used by the CLI.

type RunnerFunc

type RunnerFunc func(context.Context, Invocation) (Command, error)

RunnerFunc adapts an ordinary function to the Runner interface — handy for tests and for wrapping an existing executor without a new type.

func (RunnerFunc) Run

func (f RunnerFunc) Run(ctx context.Context, inv Invocation) (Command, error)

Run implements Runner.

type Session

type Session struct {
	Runner   Runner
	Registry *Registry
}

Session pairs a Runner (input) with a Registry (output). It is the only place the two halves meet, and it is optional sugar: callers who already have raw output just call Registry.Compress directly.

func DefaultSession

func DefaultSession() *Session

DefaultSession is ExecRunner + Default(): run a command and compress its output with the built-in filters, all in process.

func NewSession

func NewSession(runner Runner, reg *Registry) *Session

NewSession composes a Runner and a Registry.

func (*Session) Run

func (s *Session) Run(ctx context.Context, inv Invocation) (Command, Result, error)

Run executes the invocation and compresses its output. The returned Command is the raw capture (so callers can keep it); Result is the compressed view. A non-zero exit is reflected in both, not returned as an error.

func (*Session) RunStream

func (s *Session) RunStream(ctx context.Context, inv Invocation, onLine StreamFunc) (Command, Result, error)

RunStream executes the invocation with live line callbacks, then compresses the final output. If the session's Runner does not support streaming, it degrades to a batch Run (onLine is not called) so callers can use one code path regardless of runner capability.

type Sink

type Sink interface {
	// Save persists cmd's full output and returns an opaque handle (e.g. a file
	// path) the caller can later use to retrieve it. An error means "could not
	// save" — Compress then leaves FullRef empty and proceeds; recovery is a
	// best-effort convenience, never a hard dependency.
	Save(cmd Command, res Result) (ref string, err error)
}

Sink persists the full, uncompressed output of a command so a lossy Result can carry a handle (Truncation.FullRef) back to it. Configure one with Registry.WithSink; FileSink is the built-in disk implementation (rtk's "tee"). A Sink is only consulted when a Result actually dropped something.

type Spec

type Spec struct {
	Name  string    `json:"name"`
	Match MatchSpec `json:"match"`

	// MatchOutput collapses the whole output to a one-line message when a
	// pattern matches the full blob — e.g. a successful `git push` becomes
	// "ok (pushed)". Borrowed from rtk's most effective compression stage. The
	// first matching rule wins; a rule is skipped if its Unless pattern also
	// matches (so error/warning output is never hidden behind an "ok"). Checked
	// before JSON/Lines.
	MatchOutput []OutputRule `json:"match_output,omitempty"`

	JSON      *JSONSpec  `json:"json,omitempty"`
	Lines     *LineSpec  `json:"lines,omitempty"`
	Group     *GroupSpec `json:"group,omitempty"` // collapse surviving lines by a captured key
	Log       *LogSpec   `json:"log,omitempty"`   // parse a log stream into levelled Records
	Limit     *LimitSpec `json:"limit,omitempty"`
	EmptyText string     `json:"empty_text,omitempty"` // text to emit when nothing survives
}

Spec is a declarative filter definition. Exactly one of JSON or Lines should drive the transform; if both are set, JSON is tried first and Lines is the fallback when stdout isn't valid JSON.

func DefaultSpecs

func DefaultSpecs() []Spec

DefaultSpecs returns the built-in specs across all embedded ecosystem files, in a deterministic order (filename, then declaration order within a file).

func LoadSpecs

func LoadSpecs(data []byte) ([]Spec, error)

LoadSpecs decodes a JSON array of Specs.

func (Spec) Compile

func (s Spec) Compile() (Filter, error)

Compile turns a Spec into a runnable Filter, precompiling its regexps.

func (Spec) Validate

func (s Spec) Validate() error

Validate reports whether a Spec is well-formed (regexps compile, required fields present) without building a filter.

type Stats

type Stats struct {
	// contains filtered or unexported fields
}

Stats accumulates compression savings across many commands — gortk's answer to rtk's "gain" report. Wire it into a Registry as an observer:

stats := &gortk.Stats{}
reg := gortk.Default().Observe(stats.Observe)
...
fmt.Println(stats.Report()) // total saved across every Compress

The zero value is ready to use and safe for concurrent use.

func (*Stats) Observe

func (s *Stats) Observe(_ Command, res Result)

Observe records one result's sizes. Its signature matches Registry.Observe, so it can be passed directly.

func (*Stats) Report

func (s *Stats) Report() StatsReport

Report returns a snapshot of the accumulated savings.

type StatsReport

type StatsReport struct {
	Commands      int
	InputBytes    int64
	OutputBytes   int64
	SavedBytes    int64
	SavedFraction float64 // [0,1]
	DroppedLines  int64
}

StatsReport is an immutable snapshot of cumulative savings.

func (StatsReport) String

func (r StatsReport) String() string

String renders a one-line summary, e.g. "gortk: 12 cmds, 1.2 MiB -> 64 KiB (95% saved)".

type Stream

type Stream int

Stream identifies which standard stream a StreamEvent came from.

const (
	StreamStdout Stream = iota
	StreamStderr
)

func (Stream) String

func (s Stream) String() string

type StreamEvent

type StreamEvent struct {
	Stream Stream
	Line   string // the line, without its trailing newline
}

StreamEvent is one line of output observed while a command runs.

type StreamFunc

type StreamFunc func(StreamEvent)

StreamFunc receives output lines as they are produced. It is called serially (stdout and stderr never overlap), so implementations need no locking.

type StreamRunner

type StreamRunner interface {
	Runner
	RunStream(ctx context.Context, inv Invocation, onLine StreamFunc) (Command, error)
}

StreamRunner is an optional capability: a Runner that can also stream output line-by-line. ExecRunner implements it. Hosts whose executor can't stream implement only Runner; Session.RunStream degrades gracefully for those.

type Truncation

type Truncation struct {
	Happened bool

	// DroppedLines counts whole lines removed as known noise.
	DroppedLines int

	// DroppedBytes counts bytes removed by size bounding.
	DroppedBytes int

	// Masked counts in-place substitutions made by redaction (secrets) and
	// normalization (volatile tokens like UUIDs). Unlike DroppedLines/DroppedBytes
	// these don't remove a line — they replace a span with a marker — so they do
	// not affect Lossless(): the signal "something was here" is preserved.
	Masked int

	// Note is a short human/agent-readable explanation, e.g.
	// "kept 3 failing tests, dropped 412 passing".
	Note string

	// FullRef, when non-empty, is a handle to the complete, uncompressed output,
	// persisted by the Registry's Sink (see Registry.WithSink). It is the missing
	// half of "honest about loss": the loss is not only recorded but recoverable,
	// so an agent that needs a line a filter dropped can fetch the full text
	// instead of re-running the command. Empty when no Sink is configured or
	// nothing was dropped.
	FullRef string
}

Truncation describes loss introduced during compression. It is a compact, serializable record so callers can surface a "this view is partial" signal to their agent or UI.

Directories

Path Synopsis
cmd
gortk command
Command gortk is an OPTIONAL developer/debug tool for the gortk package.
Command gortk is an OPTIONAL developer/debug tool for the gortk package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL