Note
jym is an experimental project for exploring semantic CLI correction.
Suggestions can be wrong, the interface may change, and it is not intended
for production-critical workflows.
jym wraps any CLI command. When you enter a subcommand the CLI does not
document, it reads the CLI's help output and asks
Jev — TypeSafe's System One model — which
documented subcommand you most likely meant.
$ git remove foo.txt # actually an alias for: jym git remove foo.txt
jym: "remove" is not a git subcommand. Did you mean?
1) git rm foo.txt 0.93
[Enter] run 1 [o] run as typed [n] cancelUnlike an edit-distance Did you mean?, jym matches on intent: it
hands Jev the candidate subcommand names plus their help descriptions.
remove → rm, list → ps, undo → restore are close in meaning but far
in spelling — that is the gap this experiment targets.
$ go install github.com/syumai/jevyoumean/cmd/jym@latest$ jym -- git switch main
$ jym -- gh pr view 123
$ jym -- kubectl delete pod fooThe -- separator is optional but recommended. jym itself has no
subcommands — management operations are flags, so wrapping a command
named auth or setup never collides.
The intended use is as a project-local shell alias, e.g. with mise:
# mise.toml
[shell_alias]
git = "jym -- git"
gh = "jym -- gh"
kubectl = "jym -- kubectl"jym --print-mise git gh kubectl emits that snippet. jym resolves the
target with a PATH lookup, so the alias never recursively expands (a shim
or symlink to jym is skipped, and JYM_DEPTH breaks any residual
loop).
mise shell aliases only apply in mise activated interactive shells —
which is exactly where jym intervenes anyway.
To wrap selected commands without mise, add one of these lines to
.bashrc or .zshrc respectively:
eval "$(jym --shell-integration bash git gh kubectl)"
eval "$(jym --shell-integration zsh git gh kubectl)"The generated shell functions preserve argument boundaries, redirections
and exit codes. An explicitly selected command replaces an alias with the
same name. Use command git ... to bypass a wrapper temporarily.
Experimental and dangerous: --all wraps every external executable
currently visible on PATH:
eval "$(jym --shell-integration zsh --all)"Shell builtins, keywords, existing aliases/functions, and jym itself are
not wrapped. PATH changes after shell startup are not picked up until the
integration is evaluated again. This mode can misinterpret ordinary
arguments as subcommands (for example, a filename passed to rm or
bash), add startup overhead, and cause prompts in many commands. It is
not recommended as a default setup.
| Flag | Action |
|---|---|
--setup |
Prompt for and store the TypeSafe API key (verified, hidden) |
--explain |
Show extraction, Jev request/response and decision; no exec |
--refresh |
Discard the wrapped command's help cache and refetch |
--cache-clear |
Remove the whole help cache |
--print-mise <cmd>... |
Print a [shell_alias] snippet for mise.toml |
--completion <shell> |
Print a delegating completion script (bash, zsh, fish) |
--shell-integration <shell> <cmd>... |
Wrap selected commands in bash or zsh |
--shell-integration <shell> --all |
Dangerous: wrap all external commands on PATH |
--doctor |
Diagnose key, API reachability, cache and TTY state |
--debug |
Debug output on stderr (also JYM_DEBUG=1) |
--version, --help |
Check-first, never run-first: jym inspects the argument vector against
the documented subcommand tree before executing, so side effects can
never run twice.
- Gate — if stderr is not a TTY (scripts, CI, pipes), or
JYM_DEPTHshows jym inside jym, the command executes untouched. - Detect — the first non-flag argument is the subcommand candidate.
If flags precede it (the token may be a flag value), jym passes
through. A match recurses into
<cmd> <sub> --help(depth limit:max_depth, default 2). Tokens confirmed before ("learned"), configuredextra_subcommands, and<cmd>-<token>plugin executables count as valid. - Ask — only for an unknown token,
jymsends one JevChoicequestion whose criteria are the candidate names + help descriptions + a mandatory__none__escape hatch. More than 254 candidates shard into a two-phase choice. - Decide — by mode:
prompt(default): list up to 3 candidates ≥suggest_threshold, wait for one key —Enter/1-3run the corrected command,oruns as typed,n/Esc/Ctrl-Ccancel (exit 127).hint: print the list, run as typed.auto: run the correction only when p ≥auto_run_thresholdand the winner is not denylisted (rm,delete,destroy,reset,push, ...); otherwise fall back to prompt.- Without a TTY stdin,
prompt/autodegrade tohint.
- Execute — on Unix via
syscall.Exec(native signals, TTY, exit codes, job control); elsewhere via a child process with the exit code propagated.
Jev is only called on the unknown-subcommand path — valid commands never touch the network. The hot path is one cached JSON read, well under 5ms.
Without an API key or on API failure, jym falls back to edit-distance
matching (edit distance ≤ 2 or ≤ len/3, plus prefix matches),
shown without probabilities and marked (offline). When Jev answers —
even __none__ — its verdict stands and no fallback runs.
"Run as typed" on a command that then exits 0 records the token as
learned in the cache, so undocumented-but-valid subcommands (private
aliases, plugins) stop prompting.
On the first interactive run without a key, jym offers setup once
(input hidden, verified with a minimal API call, Enter to skip).
Skipping is recorded in $XDG_STATE_HOME/jym/state.json and never
re-asked; jym --setup re-runs it anytime.
Key resolution order:
TYPESAFE_API_KEYenvironment variable$XDG_CONFIG_HOME/jym/credentials.toml(mode0600)
$XDG_CONFIG_HOME/jym/config.toml (or ~/.config/jym/config.toml;
JYM_CONFIG overrides the path so mise [env] can switch per project):
mode = "prompt" # prompt | hint | auto
suggest_threshold = 0.30
auto_run_threshold = 0.95
min_confidence = 0.50 # Jev answer confidence gate
timeout_ms = 1500
max_depth = 2 # nested subcommand inspection depth
context_args = "none" # none | flags | all — args sent to the API
model = "jev-latest"
debug = false
denylist = [] # additional subcommands never auto-run
[commands.git]
help_args = ["help", "-a"] # git --help omits most subcommands
extra_subcommands = ["co", "br"] # your aliases, never prompted on
[commands.kubectl]
max_depth = 3Environment overrides: JYM_MODE, JYM_DEBUG, JYM_COLOR (always or
never; TTY detection by default), and JYM_API_ENDPOINT (endpoint override,
for tests). The standard NO_COLOR variable disables colored output.
$XDG_CACHE_HOME/jym/<command>/<key>.json, keyed by resolved executable
path + mtime + size + subcommand path + help_args. Entries carry
fetched_at (7-day TTL), is_leaf, subcommands and learned.
A rebuilt binary invalidates automatically; corruption is ignored —
the cache is fail-open.
Sent to the TypeSafe API: the command name, subcommand path, the mistyped
token, and the candidate subcommand names + descriptions. Arguments are
not sent by default (context_args = "none"); flags sends flag names
only, all sends everything. Note that wrapping an internal CLI sends
its subcommand structure to TypeSafe.
jym-eval measures the experiment — semantic vs. edit-distance matching:
$ cat evals.tsv
gh vie view
kubectl del delete
git banana # empty expected = should NOT suggest
$ jym-eval evals.tsvEach row is command <TAB> typed <TAB> expected. The report shows
correct/false-suggestion/miss counts for Jev and for the fallback, plus
latency percentiles.
docs/supported-commands.md tracks which
CLIs' help output the parser handles — a checked TODO list that doubles
as the support matrix. It is generated from
testdata/help/manifest.tsv, the single source of truth:
go run ./cmd/jym-help-capture <cmd> [<cmd> <sub>]...records real help output as a fixture (permissively-licensed CLIs only; seetestdata/help/NOTICE.md— use a hand-written synthetic fixture orsource=localotherwise) and prints a manifest stub row.- Add or edit a row with a
statusofok,partial,leaf,empty,bogus,todoorout-of-scope. go test ./internal/helptextasserts every committed fixture still matches its status — including the known-broken ones, so fixing a parser bug fails the test until the manifest is updated.go run ./cmd/jym-help-reportregenerates the doc; CI verifies it is fresh.
Not implemented (by design): flag/argument correction, command-name
correction (gti→git), intervention in non-interactive environments,
natural-language command generation, OS keychain integration.
MIT
