CLI Dispatcher

How praisonai routes your command — version, Typer subcommand, or legacy prompt/YAML

Diagram

CLI Dispatcher

📋 User Request

⚙️ CLI Dispatcher

✅ Result

python
from praisonaiagents import Agent

agent = Agent(name="dispatcher", instructions="Dispatch CLI commands to the right handler.")
agent.start("Run the deploy command with the staging environment.")

Just type your prompt — praisonai "…" uses the same modern engine as praisonai run "…".

The user types praisonai …; the dispatcher routes to Typer subcommands, version flags, bare prompts, or the legacy YAML/flag path.

PraisonAI picks one of nine paths based on what you type — and adding a new subcommand means it Just Works.

Diagram

CLI Dispatcher

📝 praisonai argv

⚙️ Route Command

🤖 Agent / Handler

✅ Result

Dispatch Paths

Diagram
graph TB
    Start[📋 praisonai argv] --> V{🔍 --version<br/>or -V?}
    V -->|Yes| Print[⚡ Print version<br/>no cli.* imports]
    V -->|No| H{🔍 --help<br/>or -h?}
    H -->|Yes| Typer1[🧰 Typer help]
    H -->|No| E{🔍 argv empty?}
    E -->|Yes| Typer2[🧰 Typer TUI]
    E -->|No| F[🔎 first non-flag positional]
    F --> K{🧠 in registered<br/>Typer commands?}
    K -->|Yes| Typer3[🧰 Typer subcommand]
    K -->|No| L{🛡 implemented<br/>legacy verb?<br/>(oracle)}
    L -->|Yes| LegHandler[🤖 Legacy handler<br/>_run_legacy]
    L -->|No| T{🛡 lone token,<br/>close typo of a command?}
    T -->|Yes| Guard[❌ No such command<br/>Did you mean … — exit 2]
    T -->|No| B{🔍 bare prompt?<br/>flags all run-supported &amp;<br/>not .yaml/.yml}
    B -->|Yes| Run[🧰 Typer run<br/>run &lt;prompt&gt;]
    B -->|No| Y{🔍 .yaml/.yml first token &amp;<br/>flags all run-supported?}
    Y -->|Yes| RunYaml[🧰 Typer run<br/>run &lt;yaml&gt;]
    Y -->|No| Legacy[🤖 Legacy<br/>YAML w/ legacy flags]

    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef route fill:#10B981,stroke:#7C90A0,color:#fff
    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff

    class Start input
    class V,H,E,K,L,T,B,Y check
    class F input
    class Print,Typer1,Typer2,Typer3,Run,RunYaml route
    class Guard,Legacy,LegHandler agent

Quick Start

  1. Check Version
    bash
    praisonai --version
    # Fast path - no heavy imports
  2. Interactive Mode
    bash
    praisonai
    # Drops into Typer's interactive TUI
  3. Get Help
    bash
    praisonai --help
    # Auto-generated help with all subcommands
  4. Use Subcommands
    bash
    praisonai chat "Build a weather agent"
    # Routes to Typer automatically
  5. Free-text Prompts
    bash
    praisonai "Build a weather agent"
    # Identical to: praisonai run "Build a weather agent"

How It Works

Diagram
LegacyTypermain()UserLegacyTypermain()Useralt[–version / -V][–help / -h][No argv][Known command][Implemented legacy verb][Mistyped verb][Bare free-text prompt][YAML with run-supported flags][Legacy-only flag]praisonai commandCheck routing rules 1-9Print version & exitShow helpAuto-generated helpInteractive TUITUI interfaceExecute subcommandCommand result_run_legacy(argv)Legacy handler (0 LLM calls)exit 2 + suggestionrun “<prompt>”Modern engine resultrun “<file>”Modern engine resultPraisonAI().main()Legacy behavior
ComponentPurposeRoute Decision
main()Entry routerApplies 9 rules in order
_find_first_command()Positional finderSkips flags and their values (value-aware), finds the first positional — so --session abc123 agents.yaml discovers agents.yaml, not abc123
_get_legacy_verbs()Legacy-verb oracleAuto-discovers the legacy dispatcher’s implemented-verb set from the authoritative LEGACY_SPECIAL_COMMANDS oracle in praisonai/cli/legacy/dispatch/argparse_builder.py. Cached under _legacy_verbs_lock; fails closed — returns the static _LEGACY_VERBS_FALLBACK mirror if the import raises, so an implemented verb is never silently re-classified as free text and billed to the LLM. The fallback is deliberately not cached so a later caller can still pick up the oracle
_is_implemented_legacy_verb()Legacy-verb guardTrue when first_cmd names a verb the legacy dispatcher implements and it is not one of the ordering-hold verbs (containers, vector-stores, held back pending #4322)
_LEGACY_VERB_ROUTING_EXCLUSIONSOrdering hold-back setVerbs withheld from the legacy-routing guard because their handler currently fabricates a success with zero network (capabilities.py:1091 → container_create); restoring routing would promote that fabrication from unreachable to user-visible. Fixed set: {"containers", "vector-stores"}
_mistyped_command_suggestions()Typo guardReturns close command matches for a lone typo that isn’t an extension of a registered command (truncations like deplo→deploy are kept and suggested), or [] when any guard condition fails
_split_global_flags()Global-flag partitionerSplits argv into (global_flag_tokens, rest) so global-only flags can be hoisted ahead of the synthesised run
_get_global_option_names() / _global_only_option_names()Root-callback introspectionReads the root Typer callback‘s options; the global-only set (root minus run) is what gets hoisted
_get_typer_commands()Auto-discoveryCached command introspection
_looks_like_bare_prompt()Bare-prompt predicateTrue when not .yaml/.yml and every flag (if any) is in run‘s supported option set (derived via _get_run_option_names()) and not in the legacy-colliding quarantine set
_looks_like_yaml_run_target()YAML→run predicateTrue when first positional ends in .yaml/.yml and every flag (if any) is run-supported and not in the legacy-colliding quarantine set
_LEGACY_COLLIDING_SHORT_OPTSLegacy-vs-modern short-option quarantine-s (legacy --save / modern --session), -f (legacy --file / modern --framework) — presence forces legacy so a previously-valid legacy invocation is never silently reinterpreted
_get_run_option_names()Auto-discovery of run‘s flagsIntrospects Typer run‘s Click parameters at dispatch; cached under a lock; value-taking vs boolean discrimination
_flag_names()Value-aware flag extractorSkips a value-taking option’s following token so a dash-prefixed value isn’t mis-classified as a flag
_build_run_argv()Argv rewriterJoins positionals into one target; keeps value-taking options with their values
TyperSubcommand & run handlerRegistered commands + bare prompts
LegacyFallback handlerPrompts or YAML files carrying a legacy-only flag

Routing Rules

main() applies nine rules in order. The first match wins.

#RuleRoute
1--version / -VVersion short-circuit — prints version, no cli.* imports
2--help / -hTyper help (auto-generated, lists every subcommand)
3No argumentsTyper interactive TUI
4First arg is a Typer commandTyper subcommand
5First arg is an implemented legacy verb (present in LEGACY_SPECIAL_COMMANDS and not in _LEGACY_VERB_ROUTING_EXCLUSIONS)Legacy handler — routed via _run_legacy(argv); the verb’s own dispatcher runs. No LLM call
6Lone positional that is a close match to a registered command and is not an extension of a registered commandExit 2 — No such command, Did you mean: …?, plus a praisonai run "<token>" escape hatch
7Bare free-text prompt (all flags run-supported like --model, --continue, --session, --output, --stream)Typer run (modern engine)
8Workflow YAML file, all flags run-supportedTyper run <file> (modern engine)
9Everything else (legacy-only flag)Legacy — a one-line stderr notice fires when a legacy-only flag is what forced the fallback

Rule 5 fires when _is_implemented_legacy_verb(first_cmd) returns True — the first token names a verb the legacy dispatcher implements (it is in the shared LEGACY_SPECIAL_COMMANDS oracle) and is not one of the ordering-hold verbs in _LEGACY_VERB_ROUTING_EXCLUSIONS. The invocation is forwarded to _run_legacy(argv) before the bare-prompt/LLM rule, so praisonai thinking status reaches the thinking handler instead of being billed as a prompt. This closed the #4327 regression where 29 implemented verbs cost ~2.5k tokens per invocation and exited 0. Rule 6 fires when _mistyped_command_suggestions(argv, first_cmd) returns a non-empty list — a lone positional token, no whitespace or path separator, not ending in .yaml/.yml, no file of that name on disk, not itself a registered command, whose difflib.get_close_matches(..., cutoff=0.8) yields a registered command that the token does not extend. A token that a command starts with (an extension like tests for test, server for serve) is dropped so it stays a valid one-word prompt. The opposite direction — a token that is a truncation of a command (deplo for deploy, versio for version) — is now kept and surfaced as a suggestion instead of being forwarded to (and billed by) the model. Rule 7 fires when _looks_like_bare_prompt(argv, first_cmd) returns True — that is, when first_cmd is non-empty, doesn’t end with .yaml/.yml, and either no dash-prefixed token appears anywhere in argv or every flag present is one that Typer run itself accepts (--model, --continue, --session, --output, --stream, …, derived at dispatch time by introspecting run‘s Click parameters). Rule 8 fires when _looks_like_yaml_run_target(argv, first_cmd) returns True — the first positional ends in .yaml/.yml and every flag is run-supported — forwarding praisonai agents.yaml … as run agents.yaml … so the workflow runs inside the modern session/output/credential/permission envelope. A single genuinely legacy-only flag (--auto, --serve, --n8n, …) still forces legacy. The short-form -s (legacy --save) and -f (legacy --file) live in _LEGACY_COLLIDING_SHORT_OPTS and also stay on legacy, so an existing script is never silently reinterpreted. If run-option discovery fails, both predicates fall back to the original conservative any-flag→legacy rule.

Second-level split inside the legacy run branch

Once an invocation reaches the legacy dispatcher’s run branch (either because the user typed praisonai run … directly, or because a legacy-only flag forced fallback there), a second predicate decides between the async-jobs API and the modern agent runner:

  • First positional is not a flag, not a reserved job verb (submit / status / result / cancel / list / stream), and either ends with .yaml/.yml (the existence check happens later, inside the runner) or names an existing file → forwarded to the modern Typer agent runner (commands/run.py run_app).
  • Anything else (a reserved job verb, a free-text token that isn’t a file, an empty positional list) → forwarded to handle_run_command (async-jobs argparse parser).

The reserved-verb guard is deliberately narrow — it matches the bare token equal to a job verb, so a same-named path with a separator (./submit) still routes to the modern engine and only a stray literal submit file in cwd is overridden. This preserves the pre-existing praisonai run submit … script contract.

Diagram
graph TB
    Start[📋 praisonai run FIRST-TOKEN] --> Flag{🔍 starts<br/>with -?}
    Flag -->|Yes| Jobs[🤖 Jobs API<br/>argparse]
    Flag -->|No| Verb{🔍 reserved job verb?<br/>submit/status/result/<br/>cancel/list/stream}
    Verb -->|Yes| Jobs
    Verb -->|No| File{🔍 ends in .yaml/.yml?<br/>(existence not required)<br/>or otherwise an existing file?}
    File -->|Yes| Runner[✅ Modern agent runner<br/>run_app]
    File -->|No| Jobs

    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef jobs fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef runner fill:#10B981,stroke:#7C90A0,color:#fff

    class Start input
    class Flag,Verb,File check
    class Jobs jobs
    class Runner runner

Never-silent legacy fallback

When an invocation reaches the legacy engine solely because it contains a flag that run doesn’t accept, praisonai prints a one-line stderr notice pointing at the modern equivalent:

Note: 'summarise this' contains a flag not supported by the modern engine;
using the legacy engine. For session continuity, --output modes and
permissions, try: praisonai run "summarise this" ...

The notice fires whenever a legacy-only flag forced the fallback — on either a prompt or a YAML workflow. A flagless .yaml/.yml reaches the modern engine and does not produce a notice.

Mistyped-verb guard

A lone token that is a close typo of a registered command exits 2 instead of being billed to an LLM.

bash
$ praisonai deploi
Error: No such command 'deploi'.
Did you mean: deploy?
To run it as a prompt instead: praisonai run "deploi"
$ echo $?
2

The guard is deliberately narrow — it fires only when a single positional token has no whitespace, no / or path separator, does not end in .yaml/.yml, names no file on disk, is not itself a command, and difflib.get_close_matches(..., cutoff=0.8) returns a registered command that the token does not extend. Extensions (tests for test, server for serve) are dropped so legitimate one-word prompts still run; truncations (deplo, versio) are kept and suggested.

You typedifflib ≥ 0.8 matchExtends the command?Outcome
praisonai deplodeploynoexit 2, suggests deploy
praisonai versioversionnoexit 2, suggests version
praisonai servservenoexit 2, suggests serve
praisonai testestnoexit 2, suggests test
praisonai deploideploynoexit 2, suggests deploy
praisonai versoinversionnoexit 2, suggests version
praisonai cofigconfignoexit 2, suggests config
praisonai memry / praisonai memoyrmemorynoexit 2, suggests memory
praisonai teststestyesroutes to run as the prompt tests
praisonai serverserveyesroutes to run as the prompt server
praisonai hellonone—routes to run as the prompt hello
praisonai shownone—routes to run as the prompt show
praisonai deploi the app—3 positionalsroutes to run as deploi the app
praisonai deploi.yaml—.yaml suffixnot blocked

Global flags

Global flags (--output-format, --json, --quiet, …) are declared on the root Typer callback, not on run. When a bare prompt or YAML target follows one, the dispatcher hoists the global flag ahead of the synthesised run.

Click accepts a group-level option only before the subcommand, so --output-format json must be emitted as praisonai --output-format json run <target>; appended after run, Click rejects it with “No such option”.

bash
praisonai --output-format json version   # → --output-format json version (Typer)
praisonai --output-format json hello     # → --output-format json run hello
praisonai --output-format json agents.yaml  # → --output-format json run agents.yaml

Global-only flags (hoisted ahead of run) — declared on the root callback and not on run:

FlagShortNotes
--output-format—text \json \… ; env PRAISONAI_OUTPUT_FORMAT
--json—alias for --output-format json
--no-color—env NO_COLOR
--quiet-qminimal output
--screen-reader—no spinners/panels
--observe-Olangfuse \langextract; env PRAISONAI_OBSERVE
--version-Valready short-circuited by rule 1

Shared flags that are NOT hoisted — declared by both the root callback and run, so they stay with run and keep their existing meaning:

FlagRoot callback meaningrun meaningResult
-oshort for --output-formatshort for --outputstays with run — praisonai "hi" -o json still means run --output json
-v / --verboseglobal verboserun verbosestays with run

Behaviour Matrix

You typeRouteResult
praisonai thinking statusLegacyrouted to thinking handler — Thinking Budget panel, exit 0, 0 LLM calls (was 1 LLM call at ~2.5k tokens before PR #4329)
praisonai install requestsLegacyrouted to install handler — pip-install wrapper runs
praisonai persistence doctor --allLegacyrouted to persistence handler
praisonai audio transcribe file.mp3Legacyrouted to audio handler
praisonai files listLegacyrouted to files handler
praisonai batches create …Legacyrouted to batches handler
praisonai rerank …Legacyrouted to rerank handler
praisonai assistants listLegacyrouted to assistants handler
praisonai compaction statusLegacyrouted to compaction handler
praisonai output configLegacyrouted to output handler
praisonai wizardLegacyrouted to wizard handler (unreachable-but-not-billed; see #4327 — the legacy handler itself raises ImportError)
praisonai containers listLegacy free-text (still billed)exclusion — held back pending #4322 so the fabricated container-ID success does not become user-visible
praisonai vector-stores listLegacy free-text (still billed)exclusion — same as containers, pending #4322
praisonai "Create a weather app"Typer runtarget="Create a weather app"
praisonai build a weather agent (unquoted)Typer runtokens joined → target="build a weather agent"
praisonai hello (single word, unknown)Typer runtarget="hello"
praisonai totally-unknownTyper runtarget="totally-unknown"
praisonai show (reserved verb, bare)Typer runtarget="show" — no ≥ 0.8 command match, runs as a prompt
praisonai memoyr (typo of memory, bare)Exit 2No such command 'memoyr'. Did you mean: memory?
praisonai agents.yamlTyper runforwarded as run "agents.yaml" — modern engine
praisonai "Create a weather app" --framework crewaiTyper run--framework is a run option; forwarded as run "..." --framework crewai
praisonai --verbose helloTyper run--verbose is run-supported; forwarded as run "hello" --verbose
praisonai "fix the auth bug" --model gpt-4oTyper runforwarded as run "fix the auth bug" --model gpt-4o
praisonai "summarise this" --continueTyper runforwarded as run "summarise this" --continue
praisonai "diagnose" --output jsonTyper runforwarded as run "diagnose" --output json
praisonai build a weather agent -m gpt-4o (unquoted)Typer runforwarded as run "build a weather agent" -m gpt-4o — value stays with its flag
praisonai "resume" --session -abc (dash value)Typer runvalue’s leading - is not mis-classified as a flag
praisonai "do it" --serveLegacy--serve is legacy-only; stderr notice fires
praisonai "x" --model gpt-4o --serve (mixed)Legacyone unrecognised flag → legacy; stderr notice fires
praisonai agents.yaml --continueTyper runforwarded as run "agents.yaml" --continue
praisonai agents.yaml --output jsonTyper runforwarded as run "agents.yaml" --output json
praisonai agents.yaml --session abc123Typer runforwarded as run "agents.yaml" --session abc123
praisonai --session abc123Bare interactive launchResumes into the TUI (no run subcommand). See Interactive TUI → Launching with a resumed session.
praisonai -cBare interactive launchResumes the most recent session into the TUI.
praisonai --session abc123 agents.yamlTyper runvalue-aware first-positional discovery finds agents.yaml; argv forwarded intact after run
praisonai agents.yaml -sLegacy-s collides with legacy --save; quarantined to legacy
praisonai agents.yaml -f input.txtLegacy-f collides with legacy --file; quarantined to legacy
praisonai agents.yaml --session abcTyper runlong form is unambiguous
praisonai agents.yaml --framework crewaiTyper runlong form is unambiguous
praisonai agents.yaml --serveLegacy--serve is legacy-only; stderr notice fires
praisonai agents.yaml --n8nLegacy--n8n is legacy-only; stderr notice fires
praisonai chat "Hello"Typerrule 4 fires before rule 6
praisonai serve / call / realtime / debug (flagless)Typerregistered commands — rule 4 wins
praisonai --output-format json versionTyperversion runs with JSON output — previously mis-routed to legacy, printed nothing useful, exit 0
praisonai -o json versionTypersame
praisonai --output-format json helloTyper runhoisted → --output-format json run hello
praisonai --output-format json agents.yamlTyper runhoisted → --output-format json run agents.yaml
praisonai "hi" -o jsonTyper run-o is shared, never hoisted → run "hi" -o json
praisonai deploiExit 2No such command 'deploi'. Did you mean: deploy?
praisonai deploExit 2No such command 'deplo'. Did you mean: deploy?
praisonai versioExit 2No such command 'versio'. Did you mean: version?
praisonai servExit 2No such command 'serv'. Did you mean: serve?
praisonai tesExit 2No such command 'tes'. Did you mean: test?
praisonai testsTyper runextension of test — a valid one-word prompt
praisonai serverTyper runextension of serve — a valid one-word prompt
praisonai deploi the appTyper runmore than one positional → free text
praisonai agents.yaml --serveLegacyunchanged — the legacy escape hatch still works
praisonai run agents.yamlTyper run (via legacy branch → run_app)Modern agent runner — PR #4424 fixed the invalid choice exit 2; PR #5606 fixed the subsequent infinite recursion between the modern runner and the legacy executor (issue #5597); PR #5636 preserves the original diagnostic on a broken PraisonAI import by capturing the sentinel before the guarded block (issue #5632)
praisonai run submit "task"Jobs API (unchanged)Reserved verb — legacy handle_run_command runs; same script contract as before
praisonai run ./submit (file named submit in cwd)Typer run (via legacy branch → run_app)Path separator dodges the reserved-verb guard

Auto-Discovery

Commands registered in praisonai/cli/app.py become routable automatically through Click introspection.

python
# In praisonai/cli/app.py
from .commands.mycmd import app as mycmd_app

def register_commands():
    # ... other commands ...
    app.add_typer(mycmd_app, name="mycmd", help="My new command")
    # That's it - no dispatcher changes needed

The auto-discovery cache (_get_typer_commands()) works by:

  1. Importing the Typer app and calling register_commands()
  2. Using Click’s introspection to list all registered commands
  3. Caching the result in _typer_commands_cache with thread safety
  4. Returning an empty set on failure (cache not poisoned for retry)

Common Patterns

Bare Prompt

bash
praisonai "Create a Python script that scrapes weather data"
# Routes to Typer run - no flags, not a .yaml/.yml file

YAML File

bash
praisonai agents.yaml
# Routes to Typer run - same modern engine as `praisonai run agents.yaml`.
# Add run flags too: praisonai agents.yaml --continue --output json

Subcommand with Global Flags

bash
praisonai --verbose chat "Hello world"
# --verbose is skipped when finding first positional (chat)
# Routes to Typer since 'chat' is a registered command

Bare Prompt With run Flags

bash
praisonai "fix the auth bug" --model gpt-4o
praisonai "summarise this" --continue
praisonai "diagnose" --output json

# All three reach the modern engine — same as praisonai run "..." <flag>.
# Session continuity, --output modes, credential gate, permissions are all in effect.

Two typo guards

There are now two guards, and they fire on different paths.

GuardWhereFires onBehaviour
Mistyped-verb guard (new)modern dispatcher, praisonai/__main__.pya lone close typo of a registered command that isn’t an extension of it, on any invocationstderr No such command + Did you mean + praisonai run "…" hint, exit 2
Reserved-verb guard (existing)legacy path, classify_unknown_command in praisonai/cli/legacy/dispatch/argparse_builder.pyonly once a legacy-only flag (--auto, --serve, --n8n, …) already forced legacystderr hint, exit 2
Diagram

Yes

No

No

Yes

🔎 lone token, no flags

🛡 close typo of
a command? not an extension

❌ No such command
Did you mean … — exit 2

🧰 Typer run
prompt to model

🔎 legacy invocation
legacy-only flag

🛡 Reserved verb
or close typo?

🤖 Direct prompt → LLM

❌ Print hint stderr
exit 2

When it fires

You typeGuard fires?What happens
praisonai show (bare)❌routes to Typer run as the prompt show — no ≥ 0.8 command match
praisonai memoyr (bare)✅modern guard fires: No such command 'memoyr'. Did you mean: memory?, exit 2
praisonai deploi (bare)✅modern guard fires: Did you mean: deploy?, exit 2
praisonai deplo (bare)✅modern guard fires: Did you mean: deploy?, exit 2
praisonai serv (bare)✅modern guard fires: Did you mean: serve?, exit 2
praisonai tests (bare)❌extension of test — routes to run as the prompt tests
praisonai show --auto✅legacy-only flag forces legacy; reserved-verb guard runs, stderr hint, exit 2

Best Practices

Legacy verbs route to their handler, not an LLM

Legacy verbs (thinking, install, persistence, audio, files, batches, assistants, rerank, compaction, output, and 15 more) are handled by the legacy argparse dispatcher, not the Typer app. The unified router consults the legacy dispatcher’s own authoritative verb list (LEGACY_SPECIAL_COMMANDS) as a shared oracle so an implemented verb is never re-classified as free text and billed to an LLM. This closed a regression introduced in 82f55c97b where 29 implemented verbs cost ~2.5k tokens per invocation and exited 0 — see PR #4329 / issue #4327.

Why –version is fast

The --version flag takes a fast path that prints version information without importing any praisonai.cli.* modules. This keeps the command responsive even if optional dependencies are broken or missing. The version check happens before any heavy imports or command discovery.

Adding a new subcommand

To add a new subcommand, simply register it in praisonai/cli/app.py using app.add_typer(). The dispatcher automatically discovers it through Click introspection with no manual updates needed to routing logic. The command becomes available immediately after registration.

Two ways to type it, one engine underneath

praisonai "build a weather agent" and praisonai run "build a weather agent" reach the same modern engine. Bare prompts — single word or multi word, quoted or unquoted — route to Typer run and inherit session continuity, --output modes, the credential gate, and permissions/checkpoints. A single word like hello or a reserved verb like show is sent to the model as a literal prompt, not blocked — neither is a close command match. The one exception is a close typo of a real command that isn’t an extension of it (deploi, memoyr, deplo, versio): the mistyped-verb guard blocks it with exit 2 and suggests the command.

My one-word prompt got blocked

Only close typos of a real command that aren’t extensions of it are blocked. If you meant a literal prompt, use praisonai run "<word>" — the error message already prints that escape hatch — or add more words so it’s no longer a lone token. hello and show are never blocked (no ≥ 0.8 command match), and tests / server stay valid prompts because they extend test / serve (contain the command as a prefix). Truncations of a command (deplo, versio) do the opposite and are caught with a suggestion.

How to force the legacy path

Add a legacy-only flag (--auto, --serve, --n8n, …) — that works on both prompts and .yaml/.yml files. A flagless .yaml now reaches the modern engine. The short-form -s (legacy --save) and -f (legacy --file) also stay on legacy to protect existing scripts; use --session / --framework explicitly if you want the modern equivalents. Flags accepted by the modern run command (--model, --continue, --session, --output, --stream, --framework, …) stay on the modern engine instead of forcing legacy. When a legacy-only flag is what triggers the fallback, a one-line notice prints to stderr — the fallback is never silent, so you can spot un-migrated flags. The reserved-verb guard only runs once you are already on legacy; the mistyped-verb guard runs on the modern path regardless.

Failure visibility

Registration errors from register_commands() propagate directly to the user — the dispatcher does not swallow them. If an optional dependency is missing or a command fails to register, you see the real error instead of silent fallback behavior. This fail-loud approach aids debugging.



Related