Launch
The CLI front door. The
pindus/induscodeprocess entry (induscode.entry:run) plus theinduscode.launchsubsystem that parses argv, renders help, expands@fileattachments, prints the model catalog, drives sign-in / OAuth, manages extension packages, and picks resume targets — all over the indusagi framework's AI, agent, and OAuth primitives. Reached by runningpindusor byfrom induscode.launch import read_invocation, ....
Table of Contents
- Overview
- Process entry (entry.py)
- The frozen contract
- Flag table and argv parsing
- Mode derivation
- Attachments (@file)
- Model catalog
- Credentials (signin / signout)
- OAuth browser sign-in
- Extension packages
- Pickers (resume + settings)
- Public API
- Key concepts
- Related pages
Overview
This area turns a raw argument vector into a fully-configured run. entry.py is the
console-script process entry; the launch/ package is the typed command-line layer.
A single frozen contract.py declares all the shapes — Invocation, OutputMode,
the flag-table types, CredentialFault, the AuthVault Protocol, CatalogFilter —
and every behavior module is written against those names: the table-driven argv
reader, the help renderer (generated from the same flag table), the @file gatherer,
the model-catalog printer, the credential command, the OAuth adapter, the
extension-package command, and the resume / settings pickers.
Launch is the front of the pipeline. It owns the full application command line and the credential surface; the boot package orchestrates dispatch and supplies the concrete on-disk vault and the Textual session picker.
launch/
contract.py frozen type surface (no I/O)
catalog.py --list-models table printer
credentials.py signin / signout + provider directory
oauth.py browser sign-in adapter + registry + Copilot device flow
packages.py install / remove / update / list / config
pickers.py resume picker + settings browser
__init__.py barrel (zero import-time side effects)
invocation/
flags.py the one declarative flag table + usage groups
read.py table-driven argv parser -> Invocation
usage.py --help generated from the table
attachments.py @file expander -> Attachments / AttachmentError
Process entry (entry.py)
pyproject.toml wires both console scripts to induscode.entry:run:
[project.scripts]
pindus = "induscode.entry:run"
induscode = "induscode.entry:run"
run() is the [project.scripts] shim. It calls main(), flushes stdout, and
treats a BrokenPipeError (the reader of our stdout — e.g. | head — went away) as
a clean early exit: stdout is reparked on /dev/null so the interpreter's shutdown
flush cannot re-raise, and the process exits 0. Otherwise it adopts main()'s
return code.
main(argv=None) is the process-level entry. It installs exactly one idempotent
logging filter that drops any record carrying the [MCP] transport-noise marker
(a no-op when BRAND.env_debug — INDUSAGI_DEBUG — is set, so diagnostics are never
hidden during debugging), then runs asyncio.run(boot(argv)) and returns the exit
code rather than raising SystemExit, so it can be called directly from tests.
import asyncio
from induscode.entry import main
# In-process; returns an exit code, never calls sys.exit.
code = asyncio.run(main(["--print", "explain", "this", "repo"]))
Importing induscode.entry runs nothing — only the console-script run() shim boots.
The boot import is deferred inside main() so import induscode.entry stays
side-effect-light.
The frozen contract
contract.py declares only shapes plus a few inert, pure helpers — no parsing, no
terminal I/O, no widgets. Behavior modules attach their exports to the induscode.launch
barrel, so consumers import from induscode.launch rather than reaching into
individual modules. The contract re-exports a small slice of framework vocabulary:
SessionInfo (as ResumeRef), ImageContent, ThinkingLevel,
and Settings.
| Type | Purpose |
|---|---|
Invocation |
The fully parsed command line: mode, flags, positionals, prompt, plus resolved typed fields (model, account, cwd, system, append_system, thinking, tools, no_tools, mcp, print, interactive, help, version, attachments) |
OutputMode |
Literal["text","json","rpc"] selecting the boot runner; is_output_mode narrows |
ThinkingEffort |
off/minimal/low/medium/high/xhigh accepted by --thinking; a superset of the framework ThinkingLevel (adds off) |
ToolName |
Closed roster of 13 built-in tool ids the --tools / --no-tools flags validate against before any tool is constructed |
FlagSpec / FlagKind / FlagValue |
One row of the declarative flag table and its value vocabularies (boolean/string/number/list) |
Attachments / AttachmentOptions |
Result of @file expansion (inlined prose + base64 ImageContent media) and the gatherer's cwd option |
CredentialFault / CredentialFaultKind |
Typed credential failure with 7 kinds + a credential_fault builder |
CredentialVerb / ProviderEntry |
The signin/signout verb literal and a directory entry (id/label/env_key/docs_url) |
AuthVault |
Protocol for the credential vault (list_accounts, default_account, put_api_key, put_oauth, auth_kind, read_usable_key, remove); boot supplies the concrete disk implementation |
OAuthCredentials |
Vault-stored browser-sign-in credentials (access/refresh/expires epoch-ms); app-owned, since the framework has no provider-object OAuth shape |
CatalogFilter |
--list-models filter: optional provider, thinking_only, images_only, and a case-insensitive search substring |
ResumeRef / SessionLoader / ResumeFault / SettingsBrowseOptions |
Resume-picker types: the SessionInfo alias, the async session loader callable, a typed resume fault, and the settings-browser options |
Flag table and argv parsing
flags.FLAG_SPECS is the single declarative option table — 17 GroupedFlagSpec
rows across five editorial groups. The reader indexes it to parse and render_usage
walks it to print help, so help and parsing can never drift. Adding a row adds both
behavior and help text.
| Flag | Aliases | Kind | Group | Purpose |
|---|---|---|---|---|
--print |
-p |
boolean | output | Run one request, print only the result, exit |
--json |
--rpc |
boolean | output | Speak the headless line protocol for a driving parent |
--interactive |
-i |
boolean | output | Force the interactive session even with a prompt |
--model |
-m |
string | model | Select the model, provider-qualified or bare |
--account |
— | string | model | Authenticate the run with a named stored account |
--thinking |
— | string | model | Set the reasoning effort (one of THINKING_EFFORTS) |
--list-models |
— | string | model | List available models (optionally filtered) and exit |
--cwd |
— | string | context | Scope the run to a working directory |
--system |
— | string | context | Replace the built-in system prompt |
--append-system |
— | string | context | Append text after the system prompt |
--resume |
-r |
boolean | context | Pick a previous session to resume |
--continue |
-c |
boolean | context | Continue the most recent session in this directory |
--tools |
— | list | tools | Allow only the named built-in tools (comma-joined or repeated) |
--no-tools |
— | boolean | tools | Disable every built-in tool for this run |
--mcp |
— | list | tools | Attach an external MCP server endpoint (comma-joined or repeated) |
--help |
-h |
boolean | meta | Show usage and exit |
--version |
-v |
boolean | meta | Show the version and exit |
read.read_invocation builds a token index over canonical names plus aliases, then
walks argv once. The grammar is textbook and entirely data-driven — there is no
per-flag branch:
--name=valueand--name valueboth bind astring/number/listflag; an inline=valuewins, otherwise the next token is consumed unless it is itself a flag.--terminates option parsing; every subsequent token is positional.- clustered short booleans (
-pi) expand to each single-letter switch. listflags accumulate across repetition and split a comma-separated token into elements.@filetokens are recorded as attachment references rather than positionals.- the parse is total: an unrecognised
--flagis tolerated as a boolean entry in the looseflagsbag (the extension-flag escape hatch) rather than rejected, so nothing is silently dropped.
The first positional becomes prompt; the rest accumulate in positionals. A final
fold derives the strongly-typed named fields, dropping an unrecognised --thinking
value or unknown --tools name.
from induscode.launch import read_invocation, read_file_references
inv = read_invocation(["-p", "--model", "openai/gpt", "hello", "@notes.md"])
assert inv.mode == "json" and inv.print is True
assert inv.model == "openai/gpt" and inv.prompt == "hello"
# @file refs are surfaced separately for the gatherer:
refs = read_file_references(["-p", "hi", "@notes.md"]) # -> ["notes.md"]
read_file_references is a thin re-walk of argv for just the @file refs, so they
can be obtained without re-deriving the whole parse.
usage.render_usage generates the --help banner entirely from FLAG_SPECS and
FLAG_GROUPS (the synopsis names pindus); there is no second, hand-maintained help
string for it to drift against.
Mode derivation
_derive_mode resolves the OutputMode from the flag bag via a fixed precedence:
# induscode/launch/invocation/read.py
def _derive_mode(flags):
if _read_bool(flags, "json"): # --json / --rpc
return "rpc"
if _read_bool(flags, "print") and not _read_bool(flags, "interactive"):
return "json" # -p one-shot
return "text" # interactive default
So the headless line protocol (--json / --rpc) wins over a one-shot --print,
which in turn implies the non-interactive json result mode unless --interactive
overrides it; with neither, the mode is the interactive text session.
pindus "refactor the auth module" # interactive text mode (default)
pindus -p "summarize this repo" # one-shot json mode, then exits
pindus --json # headless rpc line protocol
pindus -i "start here" # force interactive even with a prompt
pindus --model anthropic/claude --thinking high "..."
pindus "review" @src/main.py @diagram.png # @file attachments
Attachments (@file)
attachments.gather_attachments expands each @file ref into one Attachments
value. It resolves the ref against the cwd (via an inlined _resolve_read_path
that handles ~ expansion), classifies it by extension, enforces a per-kind size
cap, and folds it into the result:
- text files (
.md,.py,.json,.ts,.toml, … — a closed extension set) are read as UTF-8 and inlined intoAttachments.prose, each wrapped in a<file path="...">…</file>block (10MB cap) so the model sees which file each block came from. - image files (
.png,.jpg,.gif,.webp,.bmp,.svg) are base64-encoded into frameworkImageContentinAttachments.media(20MB cap). - an empty file is skipped silently; an unknown extension, an oversized file, a
missing file, or a read error raises a typed
AttachmentErrorcarrying a discriminantkind(unsupported/too-large/not-found/read-failed), never a string sentinel or a process exit.
from induscode.launch import gather_attachments, AttachmentOptions
att = await gather_attachments(["README.md", "logo.png"], AttachmentOptions(cwd="/repo"))
# att.prose has <file path="...">...</file> text blocks
# att.media has base64 ImageContent
Model catalog
catalog.print_model_catalog renders the --list-models table. It re-derives rows
from the conductor ModelCatalog (a normalized
view over the framework get_providers / get_models), applies a CatalogFilter,
sorts by provider then model id, and lays out aligned columns: Provider, Model,
Context, Max Output, Thinking, Images. Filtering is a literal case-insensitive
substring test — no fuzzy matcher, no external ranking. The output sink (CatalogIo)
and the row source (CatalogModelSource) are injectable Protocols so the layout is
unit-tested deterministically over an in-memory provider→models map.
from induscode.launch import print_model_catalog, CatalogFilter
print_model_catalog(filter=CatalogFilter(provider="anthropic", thinking_only=True, search="claude"))
# boot calls this for `pindus --list-models claude` before touching any directory
See Models for the catalog data model.
Credentials (signin / signout)
credentials.run_credential_command owns the first positional token only. It returns
handled=False when argv[0] is not a verb, so the orchestrator calls it first and
falls through to a normal launch on a miss. signin / login and signout / logout
are the recognised verbs (the natural-language aliases keep pindus login from being
mistaken for a chat prompt).
pindus signin --provider anthropic # browser sign-in preferred
pindus signin --provider openai --method api-key # force the api-key flow
pindus signin --list # read-only list of saved accounts
pindus signout --provider anthropic --account work
PROVIDER_DIRECTORY is the 14-provider api-key directory; each ProviderEntry
carries the provider's conventional env_key (the variable indusagi.ai.get_env_api_key
reads) and a docs_url. The flow:
- Resolve the provider — named via
--provider, or a numbered pick from the merged directory (PROVIDER_DIRECTORY+list_login_providers). - Prefer browser sign-in when the provider
is_oauth_capable; otherwise fall back to api-key entry, consultingget_env_api_keyfirst so an already-exported key does not need re-typing. - Validate with
validate_api_key(non-empty, ≥ 20 chars, no placeholder markers) orvalidate_account_name(≤ 50 chars,[A-Za-z0-9_-]+). - Persist through the injected
AuthVaultand return aCredentialResultcarrying a typedCredentialFault(one ofunknown-provider,invalid-key,invalid-account,name-collision,not-found,vault,aborted) on failure — never raising for an expected failure.
from induscode.launch import (
register_built_in_oauth_providers, run_credential_command, format_credential_fault,
)
register_built_in_oauth_providers() # explicit prime: anthropic, openai, github-copilot
result = await run_credential_command(
["signin", "--provider", "openai"], vault=my_vault, profile_dir="/home/u/.pindusagi",
)
if result.fault:
print(format_credential_fault(result.fault))
CredentialIo injects the three console operations the flow needs (print, ask,
ask-secret), with live defaults over stdout / input / getpass.getpass, so the
whole flow is unit-tested with an in-memory vault and no real TTY. See
Auth for the on-disk vault.
OAuth browser sign-in
The framework ships no provider-object login, so oauth.py re-choreographs a
provider.login(callbacks) flow over framework PKCE primitives
(create_pkce_pair, build_auth_url, exchange_code, refresh_token,
OAUTH_PROVIDERS):
register_built_in_oauth_providers()is the explicit prime — it walks the frameworkOAUTH_PROVIDERSconfig table and registers an adapter for each authorization-code provider (anthropic,openai), then registers the app-sidegithub_copilot_adapterwhose RFC 8628 device-grant driver has no framework config. It is idempotent and returns the registered ids. The barrel has zero import-time side effects; boot runs this as a stage.start_oauth_logindrives the adapter's login driver and persists through the injectedAuthVault;refresh_oauth_credentialsis the refresh seam.list_login_providersis the merged sign-in directory (LoginProviderrows with anauth_kindofoauthorapiKey).open_login_urllaunches the consent URL but only forhttp(s)URLs.
register_oauth_provider / get_oauth_provider / get_oauth_providers manage the
module registry; the adapter shapes are OAuthProviderAdapter, OAuthLoginCallbacks,
OAuthAuthorization, OAuthPrompt, OAuthLoginResult, and OAuthLoginError. See
framework AI facade for the underlying credential primitives.
Extension packages
packages.run_package_command is the install / remove / update / list /
config surface over the extension-package sources the launcher loads. A source is a
plain string the loader understands — an npm: spec, a git: / https: repo, or a
bare local path — and the configured set lives under the extensionPackages settings
key, persisted through the induscode.settings.PreferenceStore. Like the credential
command, it owns the first token only (handled=False on a non-command argv[0]) and
never raises for an expected failure — a bad argument is a printed line plus a non-zero
PackageResult.code.
pindus install npm:@scope/my-ext
pindus list
pindus remove npm:@scope/my-ext
pindus config
from induscode.launch import run_package_command
res = await run_package_command(["install", "npm:@scope/my-ext"], store=pref_store)
assert res.handled and res.code == 0 # idempotent under source-key matching
Pickers (resume + settings)
pickers.pick_resume_target loads the resumable sessions for the current cwd and for
every directory, merges them (merge_sessions de-dupes by path, sorts newest-first),
and chooses one:
- on a non-interactive stdin it skips the picker and resolves the newest session,
so a piped
pindus --resumestill resumes deterministically; - on a TTY it mounts the injected picker (
ResumeDeps.mount_picker). The launch-layer default raises by design — the launch layer owns no Textual mount; boot'srepl_runnerinjects a Textual-backedResumeDepsso the real session list shows.
It returns a ResumeOutcome(path, fault) — a typed ResumeFault is returned, never
raised, when a load or mount fails, so the orchestrator can fall back to a fresh
session rather than crash.
browse_settings prints a flat console listing (working/profile directories, default
model, custom-vs-default system prompt, MCP server count, and discovered resource paths
grouped by category) and pauses for a keypress. render_settings_listing is the pure
line builder, kept separate so a test can assert the content without an I/O seam. See
Sessions for the session store.
Public API
| Name | Kind | Source | Purpose |
|---|---|---|---|
run |
function | entry.py |
The [project.scripts] shim; calls main(), swallows BrokenPipeError as exit 0, then sys.exit(code) |
main |
function | entry.py |
Process entry: installs the [MCP]-noise filter and runs asyncio.run(boot(argv)); returns the exit code |
read_invocation / read_file_references |
function | invocation/read.py |
Table-driven argv parser → Invocation; the latter re-walks argv for just the @file refs |
render_usage |
function | invocation/usage.py |
Generates the --help banner from FLAG_SPECS / FLAG_GROUPS |
gather_attachments |
async function | invocation/attachments.py |
Expands @file refs → Attachments; raises typed AttachmentError |
FLAG_SPECS / FLAG_GROUPS / GroupedFlagSpec |
const | invocation/flags.py |
The single 17-row option table and its usage groupings |
print_model_catalog / CatalogIo / CatalogModelSource / catalog_source |
function | catalog.py |
Renders the aligned model table over the conductor ModelCatalog with injectable seams |
run_credential_command / CredentialResult / CredentialIo |
async function | credentials.py |
The signin / signout command; resolves a provider, prefers OAuth, returns CredentialResult |
PROVIDER_DIRECTORY / find_provider / validate_api_key / validate_account_name / format_credential_fault / is_oauth_capable / as_signin_method |
const/function | credentials.py |
The 14-provider directory, lookups, validators, fault formatter, and the oauth|api-key method vocabulary |
start_oauth_login / refresh_oauth_credentials / register_built_in_oauth_providers / list_login_providers / github_copilot_adapter / open_login_url |
async/function | oauth.py |
Browser-sign-in adapter: PKCE auth-code flow + Copilot device grant, explicit registry prime, refresh seam |
run_package_command / PACKAGE_COMMANDS / PackageResult / PackageIo |
async function | packages.py |
The install/remove/update/list/config command over the extensionPackages setting |
pick_resume_target / ResumeDeps / ResumeOutcome / merge_sessions |
async function | pickers.py |
Loads + merges resumable sessions, uses the newest on non-TTY, else mounts the injected picker |
browse_settings / render_settings_listing / SettingsBrowseIo |
async function | pickers.py |
Prints a flat settings / resource listing and pauses |
Key concepts
Single declarative flag table. flags.FLAG_SPECS is the one source of truth: the
reader indexes it to parse and render_usage walks it to print help, so help and
parsing cannot drift. Adding a row adds both behavior and help text.
Frozen contract seam. contract.py declares only shapes and inert helpers (no
I/O); every behavior module is written against those names and attaches its exports to
the launch barrel.
Typed faults over sentinels. Failures are typed unions / exceptions —
CredentialFault (7 kinds), AttachmentError (4 kinds), ResumeFault — each carrying
a discriminant plus a hint, never string sentinels or a bare sys.exit.
Injectable I/O seams. CatalogIo, CredentialIo, PackageIo, SettingsBrowseIo,
ResumeDeps, and CatalogModelSource are Protocols with live stdout / input defaults,
letting the whole flow be unit-tested with in-memory stand-ins and no real terminal.
Explicit OAuth registry prime. register_built_in_oauth_providers must be called
explicitly (boot runs it as a stage); the barrel has zero import-time side effects.
Re-choreographed OAuth over framework primitives. The framework ships no
provider-object login, so oauth.py rebuilds the provider.login(callbacks) flow over
PKCE / build_auth_url / exchange_code / refresh_token, plus an app-owned GitHub
Copilot device-grant driver the framework cannot represent.
Related pages
- Boot — consumes the launch surface in fixed order and supplies the concrete disk vault and Textual picker.
- Conductor — the
ModelCatalogthe catalog printer reads. - Sessions — the session store the resume picker loads from.
- Auth — the on-disk credential vault implementing
AuthVault. - Models — the catalog data model and
--list-models. - Settings — the
PreferenceStorethe package command and settings browser read. - CLI reference — the complete flag and subcommand reference.
- framework AI facade — the PKCE / OAuth and
ImageContentprimitives launch builds on.
