rt: Ripta's collection of tools
Expectations:
- tools read from STDIN, write to STDOUT, and hopefully print errors to STDERR;
- tools are meant to be combined with others, e.g.,
hs might be less useful
to you, because it prints file hashes in binary output instead of hex (but
enc hex converts it to hex strings).
You can install the all-in-one hyperbinary, which excludes any tools with CGO dependencies:
go install github.com/ripta/rt/hypercmd/rt@latest
or install all tools as individual binaries:
go install github.com/ripta/rt/cmd/...@latest
or pick-and-choose each tool to individually install:
- calc for arbitrary-precision arithmetic
- cg to run a command and annotate its output with timestamps
- enc to encode and decode STDIN
- grpcto to frame and unframe gRPC messages
- hs to hash STDIN
- lipsum to generate placeholder text
- place for macOS Location Services (requires macOS and CGO)
- streamdiff to help you pick out field changes off a stream of JSON
- structfiles to examine and compare a pile of structured files
- toto to inspect some protobuf messages
- uni for unicode utils
- yfmt to reindent YAML while preserving comments
or, last but not least, install a lighter version of the hyperbinary, which excludes
tools with CGO, terminal, or filesystem requirements, but compiles to WASM:
GOOS=wasip1 GOARCH=wasm CGO_ENABLED=0 go build -o rt_lite.wasm -v ./hypercmd/rt_lite
Pull requests welcome, though you should probably check first before sinking any time.
calc
Arbitrary-precision calculator. Expressions can be passed as arguments, piped
via stdin, or entered in an interactive REPL.
❯ calc '355/113'
3.14159
In REPL mode, results are stored as $N variables for reuse:
❯ calc
calc: type an expression to calculate, ".help" for help, or ^D to exit
calc:000> 2**128
340282366920938463463374607431768211456
calc:001> $0 + 1
340282366920938463463374607431768211457
Use -d to control decimal places (default 30), and -v for verbose output
showing the internal construction:
❯ calc -d 50 '√2'
1.41421356237309504880168872420969807856967187537695
❯ calc -v '22/7'
calc:000/ Construction: Multiply(Inverse(Named("1", Int(1))), Multiply(Int(22), Inverse(Int(7))))
3.142857142857142857142857142857
Sessions can be saved and loaded with .save and .load.
Functions are called with C-style syntax, e.g. sin(0), log(8, 2), or
atan2(1, 1). The library, also listed in the REPL with .show functions:
| Group |
Functions |
| Basic |
abs(x) |
| Trigonometric |
sin(x), cos(x), tan(x), asin(x), acos(x), atan(x), atan2(y, x) |
| Exponential and logarithmic |
exp(x), ln(x), log10(x), log2(x), log(x, base) |
| Roots |
sqrt(x), cbrt(x) |
| Rounding |
floor(x), ceil(x), round(x) |
| Comparison |
min(x, ...), max(x, ...) |
| Hyperbolic |
sinh(x), cosh(x), tanh(x) |
| Combinatorial |
factorial(n), gamma(x) |
Trig functions take and return radians; work in degrees by converting
explicitly, e.g. sin(45 * PI / 180).
cg
Run a command with cg run and annotate each output line with a stream
indicator: O for stdout, E for stderr, I for cg's own lifecycle messages.
At the end of the run, a one-line summary reports the exit code, wall duration,
and per-stream line counts. Bare cg is dispatch-only: it owns the run,
resolution, and mcp subcommands and no longer execs programs itself.
go install github.com/ripta/rt/cmd/cg@latest
❯ cg run -- echo hello
O: hello
I: Finished exitcode=0 in 2ms (out=1 err=0)
❯ cg run -- sh -c 'echo out; echo err >&2'
O: out
E: err
I: Finished exitcode=0 in 3ms (out=1 err=1)
The child's exit code propagates to the shell. If the child is killed by a
signal, the summary reports the signal number instead. SIGINT and SIGTERM are
forwarded to the child.
-c / --capture writes the child's stdout and stderr to $TMPDIR/cg/<ID>/
and appends a short run ID to the summary line. Resolution subcommands thread
the ID through follow-up calls:
❯ cg run -c -- sh -c 'echo out; echo err >&2'
I: Finished exitcode=0 in 3ms (out=1 err=1) id=Q3F9K2
❯ cg out Q3F9K2
/tmp/cg/Q3F9K2/stdout
❯ cg paths Q3F9K2
/tmp/cg/Q3F9K2/stdout
/tmp/cg/Q3F9K2/stderr
❯ rg -i FOO $(cg out Q3F9K2)
cg ls lists recent runs, most-recent-first; cg ls -n N overrides the
default cap of 20, and -n 0 (or any negative value) lists everything.
--state, --exit-code, --since/--before, --cwd, and --session-id
narrow the listing further; all are optional and compose with each other and
with --pool (see below for the full grammar). --cwd DIR matches a run's
recorded working directory, resolving relative paths and symlinks before
comparing. --session-id matches a run's or pool's recorded session (see
below). A signaled
run shows its negated signal number in EXIT (-15 for SIGTERM) rather than an
encoded exit code. Capture never deletes anything; cg prune is the explicit
cleanup hook:
❯ cg ls
CG ID EXIT RUNTIME COMMAND
Q3F9K2 0 3ms sh -c 'echo out; echo err >&2'
M7P4QX 42 2ms sh -c 'exit 42'
❯ cg prune # keep the 50 most recent by mtime
❯ cg prune --keep 10
❯ cg prune --older-than 7d
❯ cg prune --dry-run
EXIT and RUNTIME are right-justified to a shared width across the listing; CG
ID and COMMAND stay left-aligned. EXIT is always a bare number (or ? when a
run has none), so a wide STATE word like pool:abandoned never drags EXIT's
column wide the way a combined column would.
--output/-o picks the rendering: table (default, shown above) is
ID/exit/runtime/command; wide inserts a SESSION column after CG ID (a run's
session ID, or - for a standalone run that has none), inserts STATE after
RUNTIME, and adds SYSTEM and USER cpu-time columns before command; json prints
a {"runs": [...]} envelope, one object per run in the same shape cg meta
returns (including cwd and session_id), with manifest set instead of the
usual finished-run fields on a collapsed pool row. STATE is the same
running/finished/failed/abandoned/unknown vocabulary as --state, with a
pool: prefix on a collapsed pool row's own state:
❯ cg ls -o wide
CG ID SESSION EXIT RUNTIME STATE SYSTEM USER COMMAND
Q3F9K2 - 0 3ms finished 2ms 1ms sh -c 'echo out; echo err >&2'
M7P4QX - 42 2ms finished 2ms 1ms sh -c 'exit 42'
❯ cg ls -o json
{
"runs": [
{"id": "Q3F9K2", "state": "finished", "command": ["sh", "-c", "echo out; echo err >&2"], "cwd": "/home/rt", ...},
{"id": "M7P4QX", "state": "finished", "command": ["sh", "-c", "exit 42"], "cwd": "/home/rt", ...}
]
}
cg run takes the execution flags. -v / --verbose prefixes every line with a
timestamp and adds a started/finished preamble; --format controls the layout
using Go's time.Format syntax. --buffered defers child output until the
command finishes, grouping by stream. --log-parse json|logfmt reformats
structured log lines inline. cg flags must precede the command; everything after
the first positional or -- is passed through to the child untouched.
cg stats reports how long earlier runs of a command took. It takes the same
shape as cg run, so swap run for stats. cg flags must precede the
command.
cg stats [flags] -- COMMAND [ARGS...]
| Flag |
Default |
Meaning |
--cwd DIR |
the current directory |
The directory the runs started in. Symlinks are resolved before comparing. |
--since TIME |
none |
Only count runs started at or after TIME. Same grammar as cg ls --since. |
--exit-code EXPR |
0 |
Only count runs with this exit code. Same grammar as cg ls --exit-code. |
--samples N |
20 |
Count at most the N most recent runs per entry. The maximum is 1000. |
--pool MODE |
any |
any counts every run, none only standalone runs, and only only pooled runs. |
-o, --output |
table |
table or json. |
The first entry is the exact match. It equals the command's argv and nothing
else. It is always present, with a count of 0 when there are no samples. A
prefix entry follows for stored runs that start with the whole argv and have
more arguments. It appears only when it has samples. Each shorter prefix of the
argv that has samples comes after that, longest first.
Every stored run counts once, at the longest prefix of the query that it
matches. The levels never overlap. For make test, take stored runs of
make test ×3, make test JOBS=10 ×5, and make lint ×2. The exact entry
counts 3, the make test prefix entry counts 5, and the make prefix entry
counts 2.
The table marks a prefix row with a trailing …, so make test … reads as
"make test and more". Prefix entries in JSON have match_type prefix. A
prefix entry whose command_prefix equals the whole command holds runs that
start with it and have more arguments. An
argv of more than 32 tokens has no entry for longer runs, because cg cannot
prove that such a run starts with the whole query. That run counts at the
deepest prefix level that it matches, and prefix levels between 32 tokens and
the full argv are not reported.
cg cannot know which arguments change a duration. It reports every level. The
caller applies its own judgment.
Pooled and standalone runs never share an entry. Pool members with
parallelism above 1 compete for CPU. Under --pool any, a level with both
kinds of samples yields one standalone entry and one entry per distinct
parallelism. Standalone comes first, then pooled by ascending parallelism.
By default, cg stats counts runs that exited with code 0. Signaled runs never
count, including cancelled ones. A run that failed in 2 seconds says nothing
about how long a passing run takes.
❯ cg stats -- make test
cwd: /home/rt/projects/rt
MATCH POOL COUNT MIN P50 P90 MAX AVG LAST LAST AT COMMAND
exact - 10 4m0s 11m30s 12m0s 12m15s 10m55s 11m41s 2026-10-09T18:22:04Z make test
exact pool x4 3 6m2s 7m10s 8m5s 8m5s 7m20s 7m10s 2026-10-08T09:41:10Z make test
prefix - 5 3m1s 4m20s 5m10s 5m10s 4m15s 4m50s 2026-10-09T10:03:44Z make test …
prefix - 2 2s 1m1s 2m0s 2m0s 1m1s 2m0s 2026-10-09T17:30:12Z make …
❯ cg stats -o json -- make test
{"cwd": "/home/rt/projects/rt", "matches": [
{"match_type": "exact", "command": ["make", "test"], "pool": false,
"stats": {"count": 10, "min_ms": 240000, "max_ms": 735000, "avg_ms": 655000,
"p50_ms": 690000, "p90_ms": 720000, "last_ms": 701000,
"last_at": "2026-10-09T18:22:04Z"}},
...
]}
Durations in JSON are integer milliseconds. p50_ms and p90_ms resist a
single cold-cache run that skews avg_ms. last_ms and last_at show drift
after a dependency bump. count says how far to trust the rest. A pooled entry
carries parallelism when it is known. The exact entry that --pool only
returns with a count of 0 omits it.
A run appends one line to a history log once its meta.json is written. The log
lives outside the capture root, so cg prune and $TMPDIR cleanup do not touch
it. Its directory is $XDG_STATE_HOME/cg, or ~/.local/state/cg when
XDG_STATE_HOME is unset. The directory is mode 0700 and its files are mode
0600. cg tightens a directory or file it finds with looser modes.
| File |
Purpose |
history.jsonl |
The log. One JSON record per line. |
history.1.jsonl |
The previous generation. |
history.key |
The 32-byte key for the argv and cwd MACs. |
history.lock |
The lock that serializes appends and rotation. The first-query import holds it exclusively. Scans hold it shared. |
history.imported |
The marker that the first-query import has run. |
Appends take history.lock exclusively, so concurrent finishes never interleave
partial lines. The same lock serializes rotation. When history.jsonl passes
8 MB, it is renamed to history.1.jsonl and replaces any older generation. The
first-query import also holds the lock exclusively. Queries read both files
while holding the lock shared, so a rotation cannot land between the two reads.
Two generations hold far more runs than --samples needs. The log needs no
compaction.
A run record holds its version in v, the kind (run), the run ID, the start
time, duration_ms, the exit code, the signal, whether the run was pooled, its
parallelism, a MAC of the cwd, and a MAC for each argv prefix. Prefixes are
hashed up to 32 tokens. A longer argv also gets a MAC of its full argv, so an
exact match still works. Prefix levels between 32 tokens and the full length are
not reported. Readers skip records whose version or kind they do not know.
The log never holds your command line. It holds keyed MACs of argv prefixes and
of the cwd. Each MAC is HMAC-SHA256 under the key in history.key, truncated to
16 bytes. Argv can carry secrets, such as tokens passed as flags. A
permanent plaintext copy would outlive the short-lived meta.json. A plain hash
of make test can be reversed by hashing common commands. A keyed MAC cannot be
reversed without the key. A query can report only what its own input names,
because it recomputes the MACs from the argv and cwd it was given.
The key sits next to the log at mode 0600. Someone who can read both can test
guesses offline. That takes the same access as reading ~/.zsh_history. Losing
the key makes existing history unmatchable. cg generates a new key, and the old
records age out through rotation.
A history write is best-effort. A failure never changes the run's exit code,
meta.json, or tool output. cg run reports it on stderr. The supervisors drop
it.
The first query imports the finished runs and pools still in the capture root,
so history does not start empty. It runs when history.imported is missing, and
it skips run IDs already in the log. A pooled member takes its parallelism
from its pool manifest. A member whose pool directory is gone is skipped, since
its parallelism is unknown. Deleting history.jsonl alone does not re-import,
because the marker stays. Delete history.imported to force a re-import.
The log also holds a record for each finished pool, with the pool's wall time.
These pool records are internal. cg stats and cg_stats do not show them.
They feed two things. One is the cg_run_many auto timeout. The other is the
history object, with source: "pool", on a timed-out response.
cg note records free-form memos that outlive a single run. A note carries an
ID, a required message, an optional set of key=value tags, and a creation
timestamp. Notes persist under $TMPDIR/cg/notes/ and are shared with the
cg_note_* MCP tools. A note written from the shell is visible to an agent, and
a note written by an agent is visible here.
cg note add takes the message as an argument or on stdin. Repeat -k to
attach tags. cg note ls lists notes newest-first; -n overrides the default
cap of 20, and -k key or -k key=value filters by tag. cg note grep
searches message bodies with --text (a fixed string) or --pattern (an RE2
regex). cg note rm deletes notes by ID.
❯ cg note add 'baseline suite green at HEAD' -k run=Q3F9K2 -k branch=cg5
D2BHTQ
❯ cg note add 'migration still running; hold re-runs'
68NBND
❯ cg note ls
68NBND 2026-07-13T02:18:15Z
migration still running; hold re-runs
D2BHTQ 2026-07-13T02:18:15Z branch=cg5 run=Q3F9K2
baseline suite green at HEAD
❯ cg note ls -k run=Q3F9K2
D2BHTQ 2026-07-13T02:18:15Z branch=cg5 run=Q3F9K2
baseline suite green at HEAD
❯ cg note grep --text baseline
D2BHTQ 2026-07-13T02:18:15Z branch=cg5 run=Q3F9K2
baseline suite green at HEAD
❯ cg note rm D2BHTQ
D2BHTQ
cg mcp starts a stdio MCP server that exposes the capture-run model as native
tools, using the same on-disk storage the shell subcommands use — a run started
with cg run -c is visible to cg_list, and a run started by cg_run is
visible to cg ls. Register with Claude Code:
claude mcp add cg cg mcp
Or by hand in the MCP host config:
{
"mcpServers": {
"cg": { "command": "cg", "args": ["mcp"] }
}
}
The server registers seventeen tools:
| Tool |
Purpose |
cg_info |
Report server diagnostics: start time, uptime, cwd, session_id, capture_root, and build info. |
cg_run |
Run a command with capture; returns metadata and head/tail excerpts. |
cg_run_many |
Run a flat pool of commands with a parallelism knob and a fail policy; returns a pool ID and a per-run summary. |
cg_list |
List recent runs, most-recent-first. |
cg_meta |
Return run state and metadata. |
cg_wait |
Block until a run finishes or a timeout elapses. |
cg_cancel |
Signal a run's process group, with optional escalation. |
cg_paths |
Return absolute paths for a run's stdout, stderr, and meta.json. |
cg_stdout |
Fetch captured stdout with byte limits and head/tail windowing. |
cg_stderr |
Fetch captured stderr with byte limits and head/tail windowing. |
cg_grep |
Search captured output and return matching lines. |
cg_prune |
Evict runs by count or age. |
cg_stats |
Report how long earlier runs of a command took, as cg stats -o json does. |
cg_note_add |
Record a free-form note with an optional set of key=value tags. |
cg_note_list |
List notes newest-first, with an optional key filter and limit. |
cg_note_delete |
Delete a note by ID. |
cg_note_grep |
Search note bodies and return whole matching notes. |
Sharing depends on both sides resolving the same TMPDIR. A sandboxed shell,
such as Claude Code's Bash tool, usually gets its own TMPDIR while the MCP
server keeps the user's. cg_info reports the server's capture_root. Setting
TMPDIR to its parent lets the CLI read the server's runs, so an agent can
script cg meta, cg grep, and cg wait over many runs and keep their output
out of its context. The sandbox usually blocks writes there, so start runs with
cg_run or cg_run_many.
The notes store is shared the same way capture runs are. A note written with
cg note add is visible to cg_note_list, and a note written by cg_note_add
is visible to cg note ls.
A non-zero child exit code is data, not an MCP error: cg_run returns
successfully with exit_code: N and the caller decides how to react.
Runs survive cg mcp restarts. Each cg_run hands the child to a small
detached supervisor process whose lifetime matches the run's. Restarting the
server does not kill or lose in-flight runs; a fresh server picks them up from
the run directory. Two caveats. First, cg_wait keeps an in-process fast path
only for runs the current server started. After a restart, waits on pre-existing
runs fall back to filesystem polling. Same result, slightly coarser latency.
Second, cg_cancel signals only runs and pools the current server started. It
takes the pid from the supervisor's status pipe, never from a file under the
capture root, because anything running as the same user can write there. After
a restart, cancelling a pre-existing in-flight run is an error; use cg cancel
from a shell instead.
If a supervisor dies before recording the run's exit — a SIGKILL, say — the
run never gets its meta.json. Such a run lists as abandoned in cg ls and
in cg_list, which also accepts state: abandoned as a filter. cg prune
treats abandoned runs as evictable alongside finished ones. A run whose
supervisor still holds the run lock is live and is never pruned.
A run directory with no lock file, no pid file, and no start.json at all
carries no liveness signal whatsoever, which happens when a supervisor dies
before it can even acquire the lock. Such a run lists as unknown rather than
running in cg ls and in cg_list, which also accepts state: unknown as a
filter. When start.json is missing, both cg ls and cg_list fall back to
the run directory's mtime for an approximate elapsed time: cg ls marks it
with a ~ prefix, and cg_list sets started_at_approx: true alongside the
mtime-derived started_at.
cg ls --state and cg_list's state input both take the same enum:
all|finished|running|failed|abandoned|unknown. cg ls defaults to all,
listing every state, and cg_list does too. This is a change from
cg_list's earlier default of finished — a caller that didn't pass
state explicitly now sees running, failed, abandoned, and unknown rows it
didn't before; pass state: "finished" to keep the old behavior.
--exit-code on cg ls and exit_code on cg_list filter finished runs by
exit code: a bare N means equals, or prefix with !=, >=, >, <, or
<= for the other five comparisons. Runs with no exit code (running,
abandoned, unknown, start-failed) never match. A collapsed pool summary row
has no single exit code to compare — it always passes through untouched,
regardless of the filter; expand with --pool any (or a pool ID) to filter
individual member rows instead.
--since/--before on cg ls and since/before on cg_list filter by
start time. since is inclusive ("at or after"); before is exclusive
("strictly before"). TIME accepts a relative duration meaning ago (4h,
7d, the same grammar cg prune --older-than uses), a full RFC3339
timestamp, or a bare YYYY-MM-DD date interpreted as local-timezone
midnight. Unlike --exit-code, these bounds do apply to pool rows, using the
pool's own precise started_at — the exemption is specific to exit codes,
which pools genuinely don't have one of.
A cg mcp server mints a session_id when it starts and stamps it on every
run and pool it spawns. cg_info reports the running server's own session, and
cg_meta reports the session recorded against a run. --session-id on cg ls
and session_id on cg_list filter by it, spanning standalone runs and pool
rows alike, so an agent can list every run and pool its conversation produced
with one filter. A standalone cg run has no server behind it and carries no
session. An unmatched session lists nothing rather than erroring. The ID is
minted fresh per server process, so a cg mcp restart begins a new session.
cg_run_many runs a flat pool of commands. Each argv in commands runs
repeat times, through at most parallelism workers. Parallelism defaults to
1, which executes runs in listed order with repeats consecutive. on_error
decides what a failure does to the rest of the pool: continue (default) runs
everything, stop schedules nothing new, and kill additionally cancels
in-flight runs. cwd and env are shared across the pool. wait and
wait_timeout_ms work as in cg_run; a timeout returns the partial summary
while the pool keeps running.
cg_run_many is not a workflow engine. There are no dependencies between runs,
no conditionals, and no per-run fallback; the calling agent is the control-flow
engine. There is also no cg run-many shell counterpart, since the shell
already has xargs -P and make -j.
The result is a summary: counts, the commands array echoed once, and one flat
record per run referencing its command by index. Failed runs carry tail
excerpts of both streams, sized by excerpt_bytes with 0 disabling them,
under a 16 KB pool-wide budget; failures past the budget carry
excerpt_omitted: true instead. Successful runs carry no excerpts. Skipped and
pending runs are visible in the summary; a pending run has no run ID yet.
A pool is one more ID in the run namespace: a directory under the capture root
holding pool.json and no stream files. Members are ordinary sibling run
directories, so cg_meta, cg_stdout, cg_stderr, and cg_grep work on any
member run ID from the summary. Scheduling lives in a small detached pool
supervisor, following the same pattern as single runs. A server restart
therefore loses nothing: in-flight runs finish, pending jobs still get
scheduled, and a fresh server's cg_wait on the pool ID aggregates via
polling. A SIGKILLed pool supervisor leaves an abandoned pool, listed and
evictable like an abandoned run.
The other tools understand pools. cg_wait on a pool ID blocks until the pool
finishes and returns the same summary as the sync call. cg_list and cg ls
collapse members behind one row per pool with state and counts; the pool
filter expands them: a pool ID lists that pool's members, none lists only
standalone runs, and any lists everything uncollapsed. cg_meta on a pool ID
returns the pool state and the manifest. cg_prune evicts a pool and its
members as one unit, and never a member from under a live pool. cg_cancel
accepts pool IDs: the default SIGTERM stops scheduling and lets in-flight runs
finish, while SIGINT additionally cancels them.
Every distinct command in a pool passes the approval gate below before anything
spawns. A denial fails the whole call with nothing started, and repeat does
not multiply prompts.
A blocking cg_run, cg_run_many, or cg_wait call sends MCP progress
notifications while it waits. cg_wait sends them for a run and for a pool.
The interval is fixed at 60 seconds. The server sends them only when the client
attached a progress token to the call. Each notification carries two fields.
progress is the number of seconds elapsed. message is a status line. For a
run, it gives the elapsed time and the stdout and stderr sizes. For a pool, it
gives the elapsed time and the done and running counts. The server stops
sending before it returns, so no notification follows the result.
Keep-alives matter because MCP clients abort a tool call that stays silent for
too long. Claude Code aborts a stdio call that is idle for 30 minutes.
CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT sets that limit. The limit is 5 minutes for
network servers. Each progress notification resets the clock, so a long
wait_timeout_ms no longer trips it. Other clients differ. When the client
sends no progress token, raise CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT or the
client's equivalent for long runs.
An agent has to pick wait_timeout_ms before a run starts. The duration history
removes the guess. cg_stats takes command, cwd, since, exit_code,
samples, and pool. It returns the same JSON as cg stats -o json. cwd
defaults to the server's working directory, as it does for cg_run. The
defaults match the CLI.
cg_run and cg_run_many take a boolean wait_timeout_auto. It is off by
default, so existing callers keep the 60000 ms default. An explicit
wait_timeout_ms always wins. Otherwise the timeout is
max(1.5 x the longest expected duration, 60000) ms. The response reports the
value it used as wait_timeout_ms_used. With no history, the timeout is 60000.
The expected duration for cg_run comes from the exact, standalone match for
the command and cwd, with the default filters. Prefix matches never count, since
make says little about make test. For cg_run_many, the first source with
samples wins:
- The wall time of earlier pools with the same cwd, commands in the same order,
repeat, and parallelism, in which every member succeeded.
- An estimate from each distinct command's exact match among pooled runs at the
same
parallelism. The estimate is
ceil(commands x repeat / parallelism) x the longest max_ms. One command
without samples voids it.
- The 60000 ms default.
Every timed-out response carries elapsed_ms, measured from the recorded start
of the run or pool. This covers timed_out: true from cg_run and
cg_run_many, and finished: false from cg_wait on a run or a pool. The
response also carries a history object when samples exist. It is omitted
otherwise. Finished responses carry neither elapsed_ms nor history. cg_run
and cg_run_many report wait_timeout_ms_used on every call that waited.
{"id": "K3FQ2M", "timed_out": true, "elapsed_ms": 62000,
"history": {"source": "run", "count": 10, "p50_ms": 690000,
"p90_ms": 720000, "max_ms": 735000}}
source says where the numbers came from. run is the exact standalone match.
pool is the wall time of earlier identical pools. Both carry count,
p50_ms, p90_ms, and max_ms. estimate is the member estimate. It carries
only max_ms, which holds the estimate. The estimate counts only runs that
started before the pool did, so a pool's own members never feed its estimate. An
agent that timed out can size its next cg_wait from elapsed_ms and
history.
cg_run checks each command against an approval matcher before running it. The
default mode prompts for unmatched commands when the client supports elicitation,
and otherwise fails closed; cg mcp --blindly-allow skips the gate entirely.
Rules live in ~/.config/cg/approve.yaml (global) and .cg.yaml /
.cg/approve.yaml / .claude/cg.yaml (project), merged at startup:
version: 1
mode: enforce # enforce (default), allow-all, or deny-all
deny:
- regex: '^/tmp/'
message: do not run executables from temporary directories
allow:
- prefix: [go, test]
- prefix: [./scripts/build.sh]
- regex: '^/opt/foo/bin/[^ ]+(\s|$)'
In enforce mode the matcher checks deny rules, then allow rules, then restrict
rules, then prompts; deny always wins. Each rule matches by exact argv, prefix
tokens, glob, or regex. argv[0] is resolved to an absolute path before
matching. For exact and prefix rules the first token's shape decides how it
matches: a bare program name (go) matches the invoked basename however the
command was spelled, an absolute path pins the exact executable, and a relative
path (./scripts/build.sh) resolves against the project root. glob and regex
rules match the canonical absolute join by default and accept as_basename: true
to match the basename join instead; the shape inference covers exact and
prefix, so as_basename is rejected there. Shells and inline-code interpreters
are denied by default and cannot be re-allowed.
A restrict section adds a fourth tier below allow: a command that matches a
restrict rule but no allow rule is refused rather than prompted, so a policy
can enumerate the safe subset of a tool and fail the rest closed within that
scope. The tiers run deny > allow > restrict > prompt, so an allow
carves a command out of a restrict scope while an explicit deny still beats
everything. restrict rules take the same four kinds as allow and deny and
accept an optional message.
allow:
- prefix: [git, show]
- prefix: [git, log]
restrict:
- prefix: [git]
message: only read-only git is permitted here
Here git show and git log run, every other git subcommand is refused with
the message, and a command outside the git scope still prompts.
enc
go install github.com/ripta/rt/cmd/enc@latest
Encode and decode strings using various encodings:
a85 for ascii85;
b32 for base32 (RFC 4848 standard encoding, ABCDEFGHIJKLMNOPQRSTUVWXYZ234567);
b32c for base32 with Crockford's alphabet (0123456789ABCDEFGHJKMNPQRSTVWXYZ);
b58 for base58;
b64 for base64 (RFC 4648 standard encoding, ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/);
hex for lowercase hexadecimal;
url for URL escape/unescape; and
varsel for encoding raw bytes into Unicode variation selectors (VS-1 to VS-256).
hs
go install github.com/ripta/rt/cmd/hs@latest
Hash the input and print the resulting hash in binary bytes. Run with -h to
see the list of supported hash functions that are compiled into the binary,
which is approximately:
sha1 for SHA-1;
sha224 for SHA-224;
sha256 for SHA-256;
sha3 for SHA-3/512;
sha384 for SHA-384; and
sha512 for SHA-512.
To output hexadecimal, pipe the output to enc hex. My knowledge graph uses a
different representation for hashes, so it's useful to me to not have the hex
representation.
❯ head -n 2 hamlet.txt
To be, or not to be: that is the question:
Whether 'tis nobler in the mind to suffer
❯ cat hamlet.txt | hs sha256 | enc hex
e26671d53d74b6751373ad34768580af77847aa1513203d9a06c292617ab5c4b%
❯ cat hamlet.txt | hs sha256 | enc base64
4mZx1T10tnUTc600doWAr3eEeqFRMgPZoGwpJherXEs=%
(ICYDK, that % at the end is zsh's PROMPT_EOL_MARK.)
grpcto
go install github.com/ripta/rt/cmd/grpcto@latest
Frame and unframe raw bytes in a gRPC envelope. For example, assuming a proto
message crafted using either toto (included in this repo) or protoc --encode (the official protobuf compiler), you can frame the message using:
echo 'hello:"world"' \
| protoc --encode foo.bar.v1.Thing ./thing.proto \
| grpcto frame > message.raw
where the resulting message.raw can be sent directly to a running gRPC
service using curl:
curl -X POST --data-binary @message.raw -o response.raw -H 'content-type: application/grpc' --raw https://localhost:8443/foo.bar.v1.Thinger/Thing
and the response.raw can be unframed and decoded using protoc:
cat response.raw \
| grpcto unframe \
| protoc --decode_raw
lipsum
Generate some placeholder text, beyond just lorem ipsum. It also does some
optional rate-limiting, printing one word at a time.
go install github.com/ripta/rt/cmd/lipsum@latest
place
Talk to macOS Location Services from the command line.
go install github.com/ripta/rt/cmd/place@latest
Query as plaintext:
❯ place
Latitude: 34.009414
Longitude: -118.162233
Accuracy: 45.751999
Last observed: 2022-02-02T21:24:40-08:00
or as JSON by giving -j or --json.
streamdiff
Helps you pick out field changes off a stream of JSON.
go install github.com/ripta/rt/cmd/streamdiff@latest
It's technically usable on any stream as long as the format is one JSON per
line.
It's convenient for viewing Kubernetes resource changes over time.
For example, you can start a watch (-w) on pods (kubectl get pods) and
pipe it to streamdiff. Most fields won't be printed, except when they change.
Consider this output:
❯ kubectl get pods -o json -w | streamdiff
T+23s Pod:pomerium-cache-6c9f84b747-cr2rx
(1/2): spec.nodeName \ -> gke-vqjp-preemptible-065-38c45f41-wtnb
(2/2): status.conditions \ -> [map[lastProbeTime:<nil> lastTransitionTime:2023-06-22T06:27:43Z status:True type:PodScheduled]]
T+24s Pod:pomerium-cache-6c9f84b747-cr2rx
(1/6): status.conditions.0 \ -> map[lastProbeTime:<nil> lastTransitionTime:2023-06-22T06:27:43Z status:True type:Initialized]
(2/6): status.conditions.1 \ -> map[lastProbeTime:<nil> lastTransitionTime:2023-06-22T06:27:43Z message:containers with unready status: [cache] reason:ContainersNotReady status:False type:Ready]
(3/6): status.conditions.2 \ -> map[lastProbeTime:<nil> lastTransitionTime:2023-06-22T06:27:43Z message:containers with unready status: [cache] reason:ContainersNotReady status:False type:ContainersReady]
(4/6): status.startTime \ -> 2023-06-22T06:27:43Z
(5/6): status.containerStatuses \ -> [map[image:us.gcr.io/dc-02/gke-vqjp/pomerium-cache:v1.0.23.1390 imageID: lastState:map[] name:cache ready:false restartCount:0 started:false state:map[waiting:map[reason:ContainerCreating]]]]
(6/6): status.hostIP \ -> 10.52.0.34
T+26s Pod:pomerium-cache-6c9f84b747-cr2rx
(1/8): status.containerStatuses.0.ready false -> true
(2/8): status.containerStatuses.0.started false -> true
(3/8): status.containerStatuses.0.state.waiting map[reason:ContainerCreating] -> \
(4/8): status.containerStatuses.0.state.running \ -> map[startedAt:2023-06-22T06:27:46Z]
(5/8): status.containerStatuses.0.containerID \ -> containerd://293972feb5b498c80a585137299990c77f44ea46d6236432aba08e72108c35dc
(6/8): status.phase Pending -> Running
(7/8): status.podIP \ -> 10.53.1.92
(8/8): status.podIPs \ -> [map[ip:10.53.1.92]]
While there is still some noise, it clearly shows when the pod was assigned to
a node, when the pod finished initializing, and when it changed phases from
Pending to Running.
In addition to a running log (as above), you can also run streamdiff -i,
which updates status on the same line instead of printing a new line for
every resource update. YMMV.
❯ kubectl get nodes -o json -w | streamdiff -i
\ Node:gke-vqjp-ondemand-370-504f82ce-r0d8 status.conditions.0.{type: FrequentContainerdRestart; status: True -> False}
\ Node:gke-vqjp-preemptible-065-38c45f41-kvjd status.conditions.0.lastHeartbeatTime: 2023-06-22T06:44:18Z -> 2023-06-22T06:49:19Z
| Node:gke-vqjp-preemptible-065-38c45f41-pklf status.conditions.0.lastHeartbeatTime: 2023-06-22T06:44:15Z -> 2023-06-22T06:49:16Z
/ Node:gke-vqjp-preemptible-065-38c45f41-wtnb status.conditions.0.lastHeartbeatTime: 2023-06-22T06:45:05Z -> 2023-06-22T06:50:11Z
structfiles (sf)
Proof of concept tool to examine and compare a pile of structured files (e.g.,
Kubernetes manifests) strewn across multiple directories or files, with any
number of documents per file.
Supports YAML, JSON, TOML, HCLv2, GOB, CSV, MessagePack, and EDN as input and
output, with some caveats:
- HCLv2 output is experimental, due to the way that HCLv2 is schema-driven and
the lack of a way to represent the schema in structfiles.
- CSV does not support nested maps. CSV treats each row as a separate document.
The first row of a CSV file is assumed to be the header.
- YAML, JSON, and GOB support multiple documents in one stream.
- EDN decoding forces stringification of map keys, and does not yet support the
entire EDN spec, e.g.,
{:foo #{a 2}} still trips up the converter.
- Logfmt does not support nested maps. Each log line is treated as a separate
document.
Resulting diff currently only in unified diff of YAML (see example).
go install github.com/ripta/rt/cmd/sf@latest
For a list of supported formats and format-specific options, run sf formats:
FORMAT EXTENSIONS INPUT OPTIONS OUTPUT OPTIONS
csv .csv yes sep:string yes sep:string
edn .edn yes - yes indent:int prefix:string
gob .gob yes - yes -
hcl2 .hcl yes - yes -
json .json yes - yes indent:int no_indent:bool
logfmt .logfmt yes - yes -
msgpack .mpk .msgpack yes - yes -
toml .toml yes - yes indent:int
yaml .yml .yaml yes - yes indent:int
The simplest subcommand is eval, which reads one or more files and prints
the data back out, like a pretty-printer. The default format is JSON with
an indentation of 2 spaces.
❯ cat $dangit
{"foo":
"bar"}
❯ sf eval $dangit
{
"foo": "bar"
}
❯ sf eval -f json $dangit
{
"foo": "bar"
}
❯ sf eval -f json -o no_indent=true $dangit
{"foo":"bar"}
Of course, you can use it to convert between formats by specifying the desired
output format:
❯ cat $nabbit
{"foo":[1,2,"bar"]}
❯ sf eval -f toml $nabbit
foo = [1.0, 2.0, "bar"]
❯ rt sf eval -f hcl2 $nabbit
foo = [1, 2, "bar"]
Some formats may expect different shape data though:
❯ rt sf eval -f csv $nabbit
Error: interface conversion: interface {} is []interface {}, not string
As a special case, you can also read from STDIN by specifying stdin:// (or -),
which assumes JSON or YAML. To optionally control the format parser, use stdin://FORMAT.
❯ mj foo=bar | sf eval -f yaml -
---
foo: bar
❯ mj foo=bar | sf eval -f yaml stdin://json
---
foo: bar
❯ generate-gob | sf eval -f json stdin://gob
{"vals":[1,2,3]}
For a more advanced example, compare two directories of Kubernetes manifests
containing all-in-one manifests (foo_aio) and one-resource-per-file
(foo_each), using -k:
❯ sf diff -k ./samples/manifests/foo_aio ./samples/manifests/foo_each
--- ./samples/manifests/foo_aio
+++ ./samples/manifests/foo_each
@@ -25,7 +25,7 @@
"name": "web",
"ports": [
{
- "containerPort": 80
+ "containerPort": 8080
}
]
}
@@ -60,7 +60,7 @@
}
}
},
- "schedule": "*/1 * * * *"
+ "schedule": "* * * * *"
}
}
{
@@ -73,8 +73,8 @@
"spec": {
"ports": [
{
- "port": 80,
+ "port": 8080,
- "targetPort": 80
+ "targetPort": 8080
}
],
"selector": {
You can diff multiple files against one file by using the :: delimiter. Arguments
before the delimiter are taken as one input, while arguments after are taken as
the second input to the diff:
❯ sf diff -k ./samples/manifests/foo_aio :: ./samples/manifests/foo_each/*.yaml
You can compare piles of structured files of differing formats and control the
output format being diffed with -f
❯ sf diff -f json ./samples/configs/yaml_each ./samples/configs/toml
--- ./samples/configs/yaml_each
+++ ./samples/configs/toml
@@ -32,7 +32,7 @@
"role": "backend"
}
},
- "title": "YAML Example One"
+ "title": "TOML Example One"
}
{
"autoscaling_rules": [
@@ -68,5 +68,5 @@
"role": "frontend"
}
},
- "title": "YAML Example Two"
+ "title": "TOML Example Two"
}
For tab-delimited output, use the CSV format and set the separator to
tab: -f csv -o sep=$'\t'
toto
Some dynamic protobuf inspection tools.
go install github.com/ripta/rt/cmd/toto@latest
You can build file descriptor set, and use protoc to inspect it:
toto compile samples
cat samples/.file_descriptor_set | protoc --decode_raw
Or generate an example protobuf message and dynamically convert it to json:
toto sample | toto recode -p samples/.file_descriptor_set -f json samples.data.v1.Envelope
The toto compile step is necessary, because you can't currently parse proto
files directly in go (or at least, I wasn't able to).
uni
Unicode-related stuff.
# For a smaller installation, excluding the Unicode Han Database:
go install github.com/ripta/rt/cmd/uni@latest
# To include Unicode Han Database, which adds about 25MB to the binary:
go install -tags unihan github.com/ripta/rt/cmd/uni@latest
Size comparison:
❯ stat -f '%z %N' uni unihan
6572386 uni
34410114 unihan
List characters:
❯ uni list java cecak
U+A981 ꦁ [EA A6 81 ] <M,Mn> JAVANESE SIGN CECAK
U+A9B3 ꦳ [EA A6 B3 ] <M,Mn> JAVANESE SIGN CECAK TELU
List characters with fewer details:
❯ uni list java cecak -o hexbytes,name
[EA A6 81 ] JAVANESE SIGN CECAK
[EA A6 B3 ] JAVANESE SIGN CECAK TELU
Show only the aggregate count (-c), skipping output (-o none):
❯ uni list java cecak -o none -c
Matched 2 runes
Show only characters in a specific character category, e.g.:
# All "Pd" (punctuation, dash)
❯ uni list -C Pd
# All "S" (symbols)
❯ uni list -C S
# All "N" (numbers) that aren't "No" (other)
❯ uni list -C N,!No
# All "Lu" (letters, uppercase) and "Ll" (letters, lowercase)
❯ uni list -C Lu,Ll
# All Cyrillic uppercase and lowercase letters (i.e., excluding modifiers and subscripts)
❯ uni list -C Lu,Ll cyrillic
# All iotified Cyrillic letters not containing 'small'
❯ uni list cyrillic iotified !small
Show only characters in a specific script, e.g.:
# All Sundanese characters, by codepoint name:
❯ uni list sundanese
# All Sundanese characters, by script name, which needs the --all flag:
❯ uni list -S Sundanese --all
Show only certain codepoints by character or codepoint:
# All lowercase ASCII characters:
❯ uni list -r a-z
# Uppercase A-G and lowercase a-g ASCII characters:
❯ uni list -r A-G,a-g
# Special characters from colon (codepoint 3A) to at sign (codepoint 40):
❯ uni list -r u+3a-40
# Emojis between 🤤 and 🤗 (order does not matter):
❯ uni list -r 🤤-🤗
❯ uni list -r 🤗-🤤
# Combine filters: emojis between 🤤 and 🤗 whose name includes "hand":
❯ uni list -r 🤤-🤗 hand
Don't forget to escape ! in your shell if necessary.
List all character categories, their names, and counts:
❯ uni cats
KEY NAME RUNE COUNT
C Other 139751
Cc Control 65
Cf Format 170
Co Private Use 137468
[...]
List all scripts and counts:
❯ uni scripts
NAME RUNE COUNT
Adlam 88
Ahom 65
Anatolian_Hieroglyphs 583
[...]
Describe characters:
❯ echo 𝗀𝘨| uni describe
U+1D5C0 𝗀 [F0 9D 97 80] <L,Ll> MATHEMATICAL SANS-SERIF SMALL G
U+1D628 𝘨 [F0 9D 98 A8] <L,Ll> MATHEMATICAL SANS-SERIF ITALIC SMALL G
U+000A "\n" [0A ] <C,Cc> <control>
Map characters for fun:
❯ echo Hello World | uni map smallcaps
Hᴇʟʟᴏ Wᴏʀʟᴅ
❯ echo Hello World | uni map italics
𝐻𝑒𝑙𝑙𝑜 𝑊𝑜𝑟𝑙𝑑
Canonically compose runes:
❯ echo 감 | uni nfc
감
❯ echo 감 | uni nfd
감
Sometimes it may be useful to decompose runes before describing:
❯ echo 쭈꾸쭈꾸 | uni d
U+CB48 쭈 [EC AD 88 ] <L,Lo> <Hangul Syllable>
U+AFB8 꾸 [EA BE B8 ] <L,Lo> <Hangul Syllable>
U+CB48 쭈 [EC AD 88 ] <L,Lo> <Hangul Syllable>
U+AFB8 꾸 [EA BE B8 ] <L,Lo> <Hangul Syllable>
U+000A "\n" [0A ] <C,Cc> <control>
❯ echo 쭈꾸쭈꾸 | uni nfd | uni describe
U+110D ᄍ [E1 84 8D ] <L,Lo> HANGUL CHOSEONG SSANGCIEUC
U+116E [E1 85 AE ] <L,Lo> HANGUL JUNGSEONG U
U+1101 ᄁ [E1 84 81 ] <L,Lo> HANGUL CHOSEONG SSANGKIYEOK
U+116E [E1 85 AE ] <L,Lo> HANGUL JUNGSEONG U
U+110D ᄍ [E1 84 8D ] <L,Lo> HANGUL CHOSEONG SSANGCIEUC
U+116E [E1 85 AE ] <L,Lo> HANGUL JUNGSEONG U
U+1101 ᄁ [E1 84 81 ] <L,Lo> HANGUL CHOSEONG SSANGKIYEOK
U+116E [E1 85 AE ] <L,Lo> HANGUL JUNGSEONG U
U+000A "\n" [0A ] <C,Cc> <control>
Sort input with different collation (-l):
❯ cat input.txt
Œthelwald
Zeus
Achilles
❯ cat input.txt | uni sort -l en-US
Achilles
Œthelwald
Zeus
❯ cat input.txt | uni sort -l da
Achilles
Zeus
Œthelwald
❯ cat input.txt | uni sort -l da -r
Œthelwald
Zeus
Achilles
yfmt
Reindent YAML while preserving comments.
go install github.com/ripta/rt/cmd/yfmt@latest
This tool treats comments as nodes and therefore will not preserve comment
indentation. For example:
❯ cat in.yaml
# does this work?
foo:
- 123 # I hope
# maybe
- 456
❯ yfmt < in.yaml
# does this work?
foo:
- 123 # I hope
# maybe
- 456