Small local launcher for AI CLI accounts that need isolated authentication state.
The first version keeps account state separate instead of copying or rewriting tokens. Each launched process receives only its profile-specific state directory:
- Codex:
CODEX_HOME - Claude Code:
CLAUDE_CONFIG_DIR - Antigravity CLI: a profile-local
HOMEcontaining its.geministate - OpenCode:
XDG_CONFIG_HOME,XDG_DATA_HOME, andXDG_STATE_HOME
This prevents separate accounts from racing over one credential file and lets
each official CLI own its refresh-token lifecycle. ai-session never reads,
prints, or stores token contents.
./install.shThe binary is ai, built from cmd/ai. go install ./... produces the same
thing in $(go env GOPATH)/bin.
The build must not pass -buildvcs=false: the commit stamp the Go toolchain
embeds is what ai version and the TUI's update check compare against the
repository, and a binary built without it can only report that it does not know.
ai self-update does not rebuild from this checkout — it keeps its own clone
at ~/.config/ai/repo and rebuilds from that instead (see "Update check and
self-update" below), so wherever you happen to run install.sh from doesn't
matter to it either way.
ai profile add codex-personal codex
ai profile add codex-work codex
ai profile add claude-personal claude
ai profile add antigravity-personal antigravity
ai profile add opencode-go opencode
ai profile add deepseek opencodeA regular profile is for you, launched interactively. An app profile
is for something else — a program of your own that spawns an agent CLI as a
subprocess and points it at a state directory through its own static
config, never asking ai what that path currently means. ai run can
re-resolve a profile name on every launch; a program that isn't ai can't,
so app profiles exist to give it a path that never changes while what it
actually points at does.
ai profile add gemini-work antigravity
ai profile add gemini-personal antigravity
ai app add shiori gemini-work gemini-personal
ai app path shiori
# /home/you/.config/ai/profiles/apps/shioriPaste that path into shiori's own config wherever it expects an agent's home
directory (HOME for Antigravity, CLAUDE_CONFIG_DIR for Claude Code,
CODEX_HOME for Codex — whatever shiori's backend reads). The first member
listed becomes active. To switch which one shiori actually uses:
ai app use shiori gemini-personalai app path shiori never changes; what it resolves to does. Nothing is
copied and no new credential store is created — the app profile is a symlink
at ai app path shiori, and switching just repoints it at a different
member's real profile directory (ai app list shows the current mapping).
Members don't have to share a provider: each provider's env vars address a
subpath of its own profile directory (.../gemini-work/home,
.../codex-work/codex, and so on), and the symlink stands for the whole
directory, so whichever provider-shaped path shiori's config already points
into resolves correctly as long as the active member is that provider.
Switching to a member of a different provider than shiori was last
configured for means updating shiori's own backend selection too — that part
is shiori's integration, not something ai can do on its behalf.
There's no live switching: ai app use only has to be right for shiori's
next launch, not for one already running with the old target open.
Ordinary ai commands also accept an app name in place of a profile name,
resolving to whichever member is currently active — useful for testing an
app profile interactively without touching shiori at all:
ai run shiori # launches gemini-personal, same as ai run gemini-personal
ai login shiori
ai install shioriThe terminal title, AI_PROFILE, and any status line still name the real
member (gemini-personal), not the app (shiori) — that's the identity
actually paying for the session, and showing it as-is means every existing
indicator works unchanged.
App names and profile names share one namespace: you can't name an app the
same as an existing profile (or vice versa), and apps itself is reserved.
There's no ai app rm yet, matching plain profiles, which have no delete
command either — and a profile currently used by an app can't be renamed
from the TUI, since that would leave the app's symlink pointing nowhere.
ai-session isolates state; it does not ship the CLIs themselves. A profile
can be fully configured and still fail at launch because its command was never
installed, and exec's own message for that names neither the profile nor the
way out. The selected-profile panel therefore shows a cli line — where the
command resolves, or not installed — press i — and ai install runs the
provider's own installer:
ai install claude-personal # a profile: installs its provider's CLI
ai install codex # or a bare provider, before any profile exists| Provider | Installer |
|---|---|
| Codex | curl -fsSL https://chatgpt.com/codex/install.sh | sh |
| Claude Code | curl -fsSL https://claude.ai/install.sh | bash |
| Antigravity | curl -fsSL https://antigravity.google/cli/install.sh | bash |
| OpenCode, DeepSeek | curl -fsSL https://opencode.ai/install | bash |
These are each vendor's own documented script. The launcher deliberately keeps no package list of its own: it would be one more thing to keep correct, and wrong here means installing the wrong software.
Two details differ from typing the one-liner yourself. The script is
downloaded before it is run rather than piped straight into a shell — the
same thing ends up executing, but a truncated response cannot half-execute and
the script is on disk to read when an install goes wrong. And it runs in your
ordinary environment, not a profile's: the CLI binary is shared by every
profile, and Antigravity's private HOME would otherwise bury the install
inside one profile's state directory. In the TUI, i shows the exact command
and asks before running it.
A provider with no known installer is refused rather than guessed at.
Log in through the isolated profile:
ai login codex-personal
ai login codex-work
ai login claude-personal
ai login antigravity-personal
ai login opencode-goUpdate a profile's CLI with its supported updater:
ai update codex-personal
ai update claude-personal
ai update antigravity-personal
ai update opencode-goThis runs codex update, claude update, agy update, or opencode upgrade
respectively.
ai run uses the credentials already stored in that profile. A new profile has
no credentials until you run ai login <profile>; the login flow is performed
by the official CLI and may open a browser or ask for a device code. The
launcher intentionally does not copy the account from your normal Codex,
Claude, or Antigravity configuration.
Run a profile:
ai run codex-personal
ai run codex-work exec -- "review this repository"
ai run claude-personal
ai run antigravity-personal
ai run opencode-goCodex and Claude profiles can be launched concurrently from multiple terminals.
Every launch creates a temporary directory beneath the profile's instances/
directory for its PID lock and an instance.json recording the folder it was
launched in, while all instances continue to use the profile's
single CODEX_HOME or CLAUDE_CONFIG_DIR. This means one login per profile and
the same settings and session history in every instance. The instance directory
is removed when the CLI exits and reclaimed automatically after a crash.
Antigravity and OpenCode profiles remain exclusive: a second launch is refused while the first is running because their file-backed OAuth stores are not known to coordinate token refreshes across processes.
Antigravity does not expose a config-home override, so its entire HOME is the
profile's home/ directory. On Linux, ai-session also clears
DBUS_SESSION_BUS_ADDRESS for the child so agy uses its profile-local
~/.gemini/antigravity-cli/antigravity-oauth-token instead of silently sharing
one account through Secret Service. Commands run by Antigravity inherit that
private home too; project paths are unchanged, but host-home resources such as
~/.ssh should be referenced by absolute path when needed.
The keyring bypass is Linux-specific; until agy exposes a portable keyring or
config-home override, OAuth profiles on macOS and Windows may still resolve to
the CLI's shared OS-keyring entry.
The run command is optional when the first argument is a profile name. These
are equivalent, and any following arguments are passed to the profile's
configured command:
ai claude-personal
ai run claude-personal
ai codex-work exec -- "review this repository"If the bare argument is not an existing profile, ai returns an error instead
of guessing a provider or creating a profile.
Once the provider CLI starts it owns the screen, and nothing in its own interface says which account is paying for the session. Two markers cover that:
-
The terminal title. A launch sets the window or tab title to
ai · <profile> (<provider>)and restores the previous title on exit, using the xterm title stack. A terminal without that stack simply keeps the titleaiset, which still names the right profile. Nothing is written when output is redirected, so piped output stays clean. -
AI_PROFILEandAI_PROVIDER. Every launched process gets both, for every provider, so a shell prompt, a CLI statusline, or a tmux status bar can render the profile however you like:# a Claude Code statusline, in the profile's own settings.json echo "[$AI_PROFILE] $(basename "$PWD")"
They are also listed by
ai env <profile>. An inherited pair is stripped before a launch, so a session started from inside another session reports its own profile rather than its parent's.
Each profile can also store default arguments. They are prepended to arguments
provided at launch, but are not used for login or integration commands. Set
them in the interactive profile editor; shell-style quotes group values with
spaces without invoking a shell. The same editor accepts a short note for each
profile. Arguments typed for a single launch with p in the TUI are appended
after the stored defaults.
The terminal title and the TUI header both stop being visible the moment the
provider CLI takes over the screen. ai integrate statusline <profile> gives a
profile the best indicator its provider supports:
ai integrate statusline claude-personal
ai integrate statusline antigravity-personal
ai integrate statusline codex-workClaude Code and Antigravity render it themselves. The command merges a
statusLine into that profile's own settings.json, keeping every other key,
and the line reads AI_PROFILE out of the launch environment so it names the
profile actually in use. An existing statusLine is refused rather than
replaced: it is yours, and overwriting it would silently drop whatever it
showed.
Codex and OpenCode have no equivalent, so those profiles are marked
"indicator": "tmux" in profiles.json and launch inside a tmux session whose
status bar sits above the CLI:
ai · codex-work (codex) ~/projects/ai-session
──────────────────────────────────────────────────────────────────────
The bar survives whatever the CLI draws, which a terminal escape sequence cannot: these CLIs take the alternate screen and manage their own scroll regions. tmux's prefix is disabled so the session stays furniture rather than a multiplexer to think about.
Two details matter for correctness. Each wrapped launch gets its own tmux
socket, because tmux runs a server and an existing one would hand the CLI that
server's environment instead of the profile's isolated directories. The socket
lives under XDG_RUNTIME_DIR rather than beside the lock, because a Unix socket
path is capped near 108 bytes and the per-instance lock directory is long enough
to exceed that on its own. Stopping a wrapped instance kills its tmux server,
not just the client.
Codex also configures its own terminal title, so it will overwrite the one ai
sets; the tmux bar is the reliable indicator there.
The TUI asks GitHub whether the build is behind the repository as its main screen appears, and says so in the title bar when it is:
↑ 3 commits behind main · U
Nothing is shown when the build is current, and a check that could not run stays
quiet rather than nagging. Press r to force a fresh check and see its answer,
whatever it is, in the status line. The same answer is available from the
command line:
ai versionai 696fc1c (built 2026-08-28T11:47:41Z)
1 commit behind main · go install ./...
The comparison is between the commit the Go toolchain stamped into the binary
and the head of main, so there is no release process to keep up with. A binary
built with -buildvcs=false carries no revision and cannot be compared; ai version says so and names the fix. This is why install.sh no longer passes
that flag.
masshirodev/ai-session is private, so the check needs a token. It reads
GH_TOKEN, then GITHUB_TOKEN, then falls back to gh auth token, and
disables itself when none of the three answers. The token is only sent to
api.github.com in an Authorization header; it is never logged or written to
disk. Answers are cached for six hours in update-check.json beside
profiles.json, so a launch does not spend a request every time.
Press U, or run ai self-update, to apply it. Both rebuild from a dedicated
clone at ~/.config/ai/repo — not this checkout, and not wherever install.sh
happened to run from — so updating never depends on where you keep your own
copy. It is created on first use and kept on main from then on:
› git clone git@github.com:masshirodev/ai-session.git ~/.config/ai/repo # only if missing
› git checkout main # only if not already there
› git pull --ff-only
› sh ./install.sh
The first two lines are silent when there is nothing to do — most updates are
just the last two. install.sh is preferred over a bare go install so there
is one definition of how ai is built, including the VCS stamp the update
check itself depends on. The pull is fast-forward-only, and a refused pull
stops there: rebuilding after it would reinstall the build that is already
running and report it as an update.
The update is a keypress rather than something that happens on its own — the
check is automatic, replacing your own binary without asking is not. Once it
does run, though, finishing it is not left half-done: on success ai reopens
itself, so the session that asked for the update ends up running the binary
it just built rather than the one still sitting in memory. ai self-update at
a shell prompt lands you in the TUI; U from inside the TUI ends back in the
TUI, just on the new build. A failed update does not reopen anything — the
error stays on screen.
AI_SOURCE_DIR overrides the managed clone with an arbitrary checkout
instead, for testing self-update itself against a branch that has not been
pushed yet. It is deliberately not persisted anywhere, so it cannot be set
once and forgotten: set it only for the invocation that needs it.
AI_SOURCE_DIR=~/projects/ai-session ai self-updateA directory without a go.mod and a .git is refused rather than pulled and
built in, same as the managed clone. Unlike the managed clone, an
AI_SOURCE_DIR override is never switched to main on your behalf — it is
there so you can point at whatever branch you are testing.
Install age on both computers, then export the
profile as an encrypted bundle:
ai profile export codex-work codex-work.ai-profile.age
ai profile import codex-work.ai-profile.ageThe export includes only that profile's metadata and isolated CLI state. age
prompts for the encryption passphrase; the launcher never prints or stores it.
Imports refuse to replace an existing profile. Treat the bundle like a password
backup and delete it after transferring it if it is no longer needed. API keys
provided through environment variables, such as DEEPSEEK_API_KEY, are not
included and must be transferred separately through a secret manager. Codex's
runtime-only codex/tmp tree is omitted; Codex recreates its helper symlinks
there on the destination machine. OpenCode plugin node_modules directories
are also omitted because they are host-specific and reproducible; reinstall
them from each plugin's package.json and lockfile after importing.
Antigravity's generated caches, built-ins, updater files, logs, and helper
binaries are omitted for the same reason; its settings, conversations, and
OAuth token remain in the bundle.
Do not actively use the same imported profile on multiple computers: provider refresh tokens may rotate when used.
Running ai without arguments opens an interactive Bubble Tea TUI: one
fullscreen screen with a title bar, three columns, and a key bar.
- Profiles, on the left — every account with its provider and the two quota figures, then which of them are logged in, and the launch folder pinned to the bottom.
- The selected profile, in the middle — how it launches, what quota it has left and when that resets, how busy it has been over the last day, and what it was last working on.
- What is live, on the right — every running instance across every profile with the conversation it has open and how long it has been up, then a short log of what the launcher has done.
The title bar names the profile under the cursor next to the profile count, so the screen says which account is in play rather than leaving it to the row highlight, and it carries the launch folder and any pending update.
Everything else — the profile editor, the folder and argument prompts, the delete confirmation, and the instance pickers — opens as a box over that screen, so the cockpit stays put while you answer.
Narrow terminals fold columns away rather than squeezing them: under 118 columns the live panel merges into the middle, and under 78 the whole thing stacks into one column. Short terminals drop panels in order of what a glance can afford to lose — the activity histogram first, then the log, then the recent list.
Providers are colour-coded, a profile with active launches is marked ▶ running
or ▶ N running, and the AUTH block shows whether the profile has logged in:
● yes— the provider's credential file exists in the isolated state directory, soai runcan use it.● key— adeepseekprofile withDEEPSEEK_API_KEYexported, or an Antigravity profile withmodelProvider: "gemini"andGEMINI_API_KEY. These authenticate from the environment rather than a stored OAuth file.○ no— no credentials yet; presslto log in.· ?— the provider has no known credential location, so the launcher does not guess.
The cli field shows where the profile's command resolves on PATH, or
not installed — press i. See
Install the provider CLIs.
The model field shows the model the profile will start with, read from that
CLI's own settings inside the isolated directory. — means the provider has no
discoverable answer yet:
| Provider | Source |
|---|---|
| Codex | codex/config.toml, top-level model |
| Claude Code | claude/settings.json, model |
| OpenCode | state/opencode/model.json, most recent entry; falls back to model in config/opencode/opencode.json[c] |
OpenCode's state file is preferred because it holds the model last chosen in the TUI, which is what OpenCode restores on the next start; the config default only applies before anything has been picked.
The 5H and 7D columns, and the QUOTA meters beside them, show the
remaining five-hour and weekly quotas reported by the selected account's own
local CLI cache, along with when each window rolls over. Codex is read from the
newest rate-limit events in that profile's session logs; Claude Code is read
from its cached usage utilization. Expired or unavailable windows show —.
Antigravity, OpenCode, and DeepSeek do not currently expose comparable local
quota caches that this launcher reads. This is intentionally profile-local and
does not use OpenUsage, so multiple accounts for the same provider stay
separate. Credential files are never opened.
The check only tests whether the file exists, and reads the API key variable
only to see whether it is empty; ai-session still never opens or prints
credentials. A profile whose token has expired therefore keeps showing ● yes
until the official CLI asks you to log in again.
The TUI starts in the directory where you ran ai. Press c to set another
launch folder without leaving the TUI; relative paths resolve from the current
launch folder and ~ is supported. The chosen folder applies to subsequent
CLI launches in that TUI session and does not change the parent shell's
directory.
ACTIVITY counts sessions touched per hour over the last day, from the
timestamps on the profile's own transcripts. It is deliberately not labelled as
quota: no provider records what a limit cost at a given hour, so the histogram
measures the one thing that is actually on disk. RECENT SESSIONS reads the
same transcripts for what each conversation was about, skipping the preamble
both CLIs write before the first thing you actually typed. Two shapes are
handled, because the CLIs use both: a block sent ahead of the prompt is
skipped whole (environment dumps, harness reminders, Codex's AGENTS.md
instruction dump), while a block wrapped around the prompt is unwrapped
instead — Codex's IDE integration leads with your open tabs and labels the part
you typed, so skipping the message would lose the prompt with it. R turns that panel
into a picker and resumes the row you choose.
Use the arrow keys or j/k to select a profile:
- Enter runs the selected profile.
pruns it with extra arguments typed at the prompt.Rresumes one of the recent sessions, in the folder it ran in.hhijacks a running instance: it opens that instance's conversation in this terminal, leaving the original process running.Hhands a session to another account, for when one has run out of quota.Aturns auto-swap on or off. Off by default.llogs in to the selected profile.iinstalls the selected provider's CLI, after showing the command.uupdates the selected provider CLI.Uupdatesai-sessionitself, after showing the checkout and the steps.cchanges the folder used for subsequent CLI launches.aadds a profile.eedits its name, provider, command, default arguments, or note.rrefreshes locally cached usage percentages and re-runs the update check.xdeletes it and its isolated state after confirmation.Kselects a running instance to stop; Enter stops that instance, whileaorystops every instance for the selected profile./filters the profile list by name or provider. Enter keeps the filter and hands the keys back, so a search is a way to reach one account among many rather than a mode to dismiss before acting; Escape clears it.?opens the key pane, and any key closes it.qor Escape quits.
The key bar along the bottom drops entries from the end until it fits, so on a
narrow terminal it is not the whole list — and the entries it drops are exactly
the ones nobody has learned yet. ? is the whole list, grouped by what each key
acts on: a conversation, a profile, a provider's CLI, or ai-session itself. It
folds to one column rather than clipping when the terminal is too narrow for
two.
Both instance pickers — h and K — list each running instance with the
conversation it has open and the folder it was launched in, so two instances of
the same profile can be told apart by what they are doing rather than by PID.
The resume picker — R — lists transcripts rather than processes, so its rows
are dated instead of numbered.
H is for the case where one CLI's limit is spent and the work is not finished.
It reads the outgoing conversation, reduces it to a brief, and starts another
account on that brief in the same folder.
Nothing is copied into either CLI's state directory. The conversation stays
where it was recorded; what moves is a markdown file under
~/.config/ai/handoffs/.
The brief has three parts:
- What was asked — every request you typed, in order, with the CLIs' own injected blocks stripped and the half-written copy of an interrupted message dropped.
- Where the previous agent left off — its last few substantial messages. Substance is measured by length, which is crude but free: a model's final messages are usually its shortest, so taking the tail by position picks the line between two tool calls rather than a conclusion.
- The repository, as of the handoff — branch, uncommitted changes, diff
against
HEAD, recent commits. This is read from git at handoff time rather than reconstructed from the transcript, because what a transcript records depends on how the previous agent happened to hold its tools. On one session here, recovering the touched files from tool arguments found two of the six that were actually edited, because the work went through shell heredocs and no tool argument ever named a path. Git does not have that problem.
The full transcript is named at the end as a path, not pasted in. That is the difference from handing over the profile folder and saying "continue": the next agent can read it, but does not have to. On this machine a 1140 KB transcript (~292k tokens) reduced to a 5 KB brief (~1.3k tokens).
There is no model anywhere in that path, and that is forced rather than chosen:
the premise is that you are out of quota, so the outgoing CLI cannot summarise
itself, and asking the incoming one to summarise means reading the transcript —
the cost being avoided. Extraction is mechanical. Press e at the confirmation
to edit the brief before it goes; you know what mattered.
Destinations are ranked by the quota window that runs out soonest, since a weekly allowance with room is no help at the moment the five-hour one is spent. An account whose quota is unknown sorts below a measured one.
A handoff is recorded in ~/.config/ai/lineage.json, and RECENT SESSIONS
marks the source row with → <account>. The chain reads forwards only: a
handoff is a baton pass, not a fork, so there is never a newer branch on the
other side to reconcile.
A toggles auto-swap, which is off by default and persisted in
profiles.json as settings.auto_swap.
With it on, H skips the destination question and sends the work to whichever
account has the most quota left. It does not skip two other things:
- Which session is leaving is always asked. That is the one thing the tool cannot infer safely.
- The brief is still shown before anything launches. Writing a file and starting a process are different promises, and with auto-swap on this is the frame where you find out where the work went.
Auto-swap does not watch a running session and switch mid-flight. While a CLI owns the terminal the launcher is not running, so it has nothing to watch with.
H can hand off from Claude Code and Codex, which are the providers whose
transcripts are read. It can hand off to Claude Code and Codex, which are the
providers whose opening-prompt syntax is known. Another provider chosen as a
destination is refused with the brief's path, rather than launched with the
brief silently dropped.
R offers the conversations RECENT SESSIONS has already read off disk, and
reopens the chosen one by id in the folder it ran in. Both halves matter: the id
names the conversation, and every provider looks for that id under the folder it
belongs to, so resuming from anywhere else reaches a different conversation or
none at all. A recorded folder that has since been moved or deleted is reported
rather than quietly swapped for the current launch folder.
This is a wider offer than the provider's own resume flow, which only ever sees
the folder it was started in. The panel has read every folder the account has
worked in, so a conversation from another project is one keypress away instead
of a cd away.
The list is always the selected profile's own, and it is reopened under that profile's environment. One account cannot resume another's conversation: a session id only exists inside the isolated state directory that recorded it, so the same id under a different profile finds nothing.
What RECENT SESSIONS reads is per provider. Codex and Claude Code keep a
transcript file per conversation, which is what gets parsed for a title.
OpenCode keeps its own SQLite database (opencode.db) with title, folder, and
timestamp as plain columns — no parsing needed, and resuming picks the exact
conversation by id (opencode --session <id>), the same as Codex and Claude.
Antigravity is not read yet: its conversation store is a per-conversation
SQLite database whose readable metadata carries ids but not a title, and the
title lives in a protobuf blob with no published schema — resuming a specific
Antigravity conversation already works from a running instance (see h
below), but there is no picker for a stopped one.
With nothing recorded — an account that has not run yet, or Antigravity, whose
transcripts this launcher does not read — R falls back to the provider's own
resume flow in the current launch folder:
| Provider | Command |
|---|---|
| Codex | codex resume (session picker) |
| Claude Code | claude --resume (session picker) |
| Antigravity | agy --continue |
| OpenCode | opencode --continue |
Antigravity and OpenCode continue the last session for the current workspace instead of opening a picker.
h chooses from processes rather than from transcripts. It reads the running
instances the launcher already tracks, resolves the session each one has open,
and reopens exactly that session in the folder the instance was launched from —
which is what makes it the key for a conversation that is still going, where R
is the key for one that has stopped. Session titles come from the provider
itself and are best effort:
| Provider | Source |
|---|---|
| Codex | the newest session log recorded for that folder |
| Claude Code | claude agents --json, matched on the recorded PID |
| Antigravity | not available; the instance is listed without a title |
| OpenCode | not available; the instance is listed without a title |
Hijacking an Antigravity or OpenCode profile is refused for the same reason a second launch is: its credential store is exclusive while the first process is running.
The selected-profile panel shows its default arguments and note. In the profile editor, Enter or Tab advances through all fields; on the final field it saves. Escape cancels and Ctrl-U clears the current field.
DeepSeek should be configured in the OpenCode profile using OpenCode's normal
provider setup or an environment variable. The launcher preserves ordinary
environment variables, including DEEPSEEK_API_KEY, but removes shared
CODEX_HOME, CLAUDE_CONFIG_DIR, XDG paths, and any inherited AI_PROFILE or
AI_PROVIDER before adding the selected profile's values.
OpenUsage remains the usage/limits layer. Install its official integrations inside each isolated profile:
ai integrate openusage claude-personal
ai integrate openusage codex-personal
ai integrate openusage codex-work
ai integrate openusage opencode-go
ai integrate openusage deepseekThe launcher deliberately does not modify OpenUsage's database or copy tokens.
It runs OpenUsage's supported installer with the selected profile environment,
so Codex and Claude hooks are installed beside that profile's own state. Login,
integration, and export remain exclusive operations and are refused while any
instance is running. Antigravity and OpenCode also retain this exclusive lock
for ordinary runs. If a launcher is interrupted, its lock is reclaimed
automatically after all PIDs recorded in it have exited. Different profiles can
still run at once.
In the TUI, select a running profile and press K to choose an individual CLI
process by PID, or stop all of that profile's instances. Each lock records the
launcher PID on its first line and the child CLI PID on its second line;
orphaned locks can therefore be reclaimed after an interrupted SSH session.
MIT. See LICENSE.