使用 Qwen Code 构建的最小自主代理 CLI
A small code-agent CLI with file and shell tools. Built with Node.js 22, TypeScript, and ESM.
oh-my-cli is a small, self-hosted code agent: a terminal CLI that reads and edits files and runs shell commands under a strict, inspectable safety plane — against any OpenAI-compatible endpoint.
What makes it different:
New here? Start with Install and the first-run guide.
Product improvements enter the autonomous queue from three sources: promoted
user reports, findings from the bounded community-source registry, and
reproducible self-discoveries. Each becomes a normalized Issue authored by the
repository Bot before execution. The autonomy contract, its policy files,
GitHub workflows, and CODEOWNERS form a protected governance plane maintained
by qqqys; the Bot may propose governance changes but cannot apply them.
简体中文用户请阅读 docs/ZH-CN.md:安装、DashScope / OpenAI 兼容端点配置、CLI 与 Desktop 启动、会话与文件安全边界、常见故障排查。 英文文档为权威版本,两者不一致时以英文为准。
npm install
npm run build
npm link
npm link puts the oh-my-cli command on your PATH so the examples below run
as written; npm unlink -g oh-my-cli removes it later. Prefer not to link?
Invoke the built entry directly: node dist/index.js.
New here? Follow docs/FIRST-RUN.md for a verified path from
install through your first successful task, including a setup --doctor check
and troubleshooting.
Model configuration is resolved from environment variables, an optional trusted
workspace .env file, and an optional user settings file. Environment variables
always take precedence, so existing export-based setups work unchanged.
| Variable | Required | Default | Description |
|---|---|---|---|
OPENAI_API_KEY |
Yes¹ | — | API key for the provider |
OPENAI_BASE_URL |
No | https://api.openai.com/v1 |
OpenAI-compatible base URL |
OPENAI_MODEL |
Yes¹ | — | Model name |
¹ Not required in the environment when supplied through the user settings file below.
To avoid exporting variables in every shell, store the non-secret model
configuration in the user-owned file ~/.oh-my-cli/settings.json, or select an
alternative with --settings . The credential itself is never stored in
the file — only the name of the environment variable that holds it:
{
"model": {
"baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"name": "qwen-latest-series-invite-beta-v77",
"apiKeyEnv": "DASHSCOPE_API_KEY"
},
"mcpServers": {},
"extensions": {}
}
This is the same file that backs the --health MCP/extension inventory. Each
field is resolved with the following precedence (highest first):
| Field | 1 (highest) | 2 | 3 | 4 (lowest) |
|---|---|---|---|---|
| Base URL | OPENAI_BASE_URL |
workspace .env OPENAI_BASE_URL |
model.baseUrl |
built-in default |
| Model name | OPENAI_MODEL |
workspace .env OPENAI_MODEL |
model.name |
(required) |
| Credential | OPENAI_API_KEY |
workspace .env OPENAI_API_KEY |
env var named by model.apiKeyEnv |
(required) |
Security: the settings file is only ever the user-owned default or a path you
pass explicitly — a settings file inside a project is never auto-discovered, so
an untrusted repository cannot redirect your endpoint or credential. Raw
credential fields such as model.apiKey are rejected; reference an environment
variable through apiKeyEnv instead. oh-my-cli --preflight prints a redacted
summary of the resolved model, endpoint host, settings source, and credential
variable name (never the credential value), marking which layer supplied each
value — env, workspace-env, settings, or default.
To keep model credentials in a project-local file instead of exporting them in
every shell, place them in .env at the workspace root:
OPENAI_API_KEY=your-api-key-here
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
OPENAI_MODEL=qwen-latest-series-invite-beta-v77
Loading is gated by folder trust: only a workspace trusted via the user-owned
trust store (--trust-workspace) or --trust for the current run has its
.env read; an untrusted workspace's .env is never opened, and a missing
file is a silent no-op. Variables already exported in your shell always win
over .env values, and .env values sit above the settings file. The file is
parsed for model-config resolution only — process.env is never mutated, so
.env values are not inherited by spawned tools or child processes, and
diagnostics report only the file path and variable names, never values.
.env.local/.env.production variants and nested .env files are not read.
To keep several named configurations (for example a hosted Qwen endpoint and a
local model) and switch between them, declare a profiles map in the same
user-owned settings file. Each profile has the same shape as the model section
(plus an optional description and a disabled flag); no raw credential is ever
stored — only the environment-variable name that holds it:
{
"defaultProfile": "qwen",
"profiles": {
"qwen": {
"baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"name": "qwen-latest-series-invite-beta-v77",
"apiKeyEnv": "DASHSCOPE_API_KEY",
"description": "Hosted Qwen"
},
"local": { "name": "llama3", "baseUrl": "http://127.0.0.1:11434/v1" }
}
}
Selection precedence (highest first): an explicit --profile , then
settings.defaultProfile, then the legacy single model section. The chosen
profile reuses the exact resolution above, so OPENAI_* still override it and
the same credential rules apply.
oh-my-cli --list-profiles # inventory (read-only, redacted)
oh-my-cli --list-profiles --output json # machine-readable
oh-my-cli --profile qwen -p "Summarize README" # select a profile for this run
oh-my-cli --preflight --profile local # verify a profile's endpoint/credential
Profiles are read only from the user-owned scope: a project-local settings file
can never set profiles or defaultProfile, so an untrusted repository cannot
silently replace your selected endpoint or credential. Unknown profile names,
disabled profiles, and missing credentials all fail before any request, and the
errors (like the listing) are redacted.
The selected profile is recorded in the session's metadata, so resuming a session under a different profile (or model) prints a redacted warning that explains the change while the conversation, tool, and approval history are preserved.
oh-my-cli -p "List the files in this directory"
oh-my-cli
While a response is streaming, the read-only commands /status, /model,
/settings, /tools, /capabilities, /continuity, and /help run
immediately: their redacted output appears in place while the turn continues
undisturbed. Anything else you type mid-stream waits — a prompt draft is kept
and submits once the turn settles, and Esc interrupts it.
Review the post-Desktop Remote Control and Dynamic Workflow surfaces on separate browser-native feature pages, with their defining behavior, live interactions, and GitHub delivery evidence. Remote Control shows the connected phone and secure session handshake; Dynamic Workflow shows a branching, parallel execution graph with an approval gate:
oh-my-cli --delivery-web
# open http://127.0.0.1:4317
The server listens on the loopback interface only. Choose another available
loopback port with --web-port . Open /remote-control or
/dynamic-workflow directly to show one feature. The pages contain no Computer
Use demo and do not expose credentials, settings, workspace paths, or a general
file server.
The native Desktop shell (Electron) zooms layout-independently: Ctrl/Cmd with the plus key zooms in, Ctrl/Cmd with the minus key zooms out, and Ctrl/Cmd+0 resets to 100% — the chords work whether or not your keyboard layout needs Shift to type "+" or "-" (the numpad add/subtract keys work too), and zoom is bounded between 50% and 200%. The same actions are available under View → Zoom In / Zoom Out / Actual Size.
oh-my-cli --resume
--resume also accepts a user-owned session name: if no session exists under
the exact id, the value resolves as the name set with --rename-session
(e.g. oh-my-cli --resume "auth refactor"). An exact id always wins; a name
shared by several sessions fails closed listing their short ids, and corrupt
or unknown values fail closed with an actionable reason — it never resumes a
different session.
The same id-or-name targeting applies to every other session-targeted flag:
--session-stats, --tasks, --export-session, --rename-session,
--compact, --undo-turn, --redo-turn, and --session all accept an
exact session id or a user-owned name, with the same fail-closed semantics
(exact id wins; ambiguous, corrupt, and unknown values are reported, never
substituted).
For automation, oh-my-cli --list-sessions --output json emits a versioned
record (schema oh-my-cli.sessions) with the resumable/corrupt totals and
one structured entry per session (id, redacted name/model/workspace, usage
counts, token estimate, timestamps, corrupt flag) — the same data the text
listing shows, redacted the same way. Add --filter to keep only
sessions whose id, name, model, or workspace contains the text
(case-insensitive substring); totals reflect the filtered set in both modes.
Prefer one step? oh-my-cli --continue resumes the most recent healthy
session declared for the current workspace — no id needed (workspace aliases
and linked worktrees match the same sessions). With no matching session it
fails closed instead of silently starting fresh, and it never resumes another
workspace's session; combine it with -p for a headless follow-up turn.
--continue cannot be combined with --resume or --browse-sessions.
Prefer to browse instead of copying an id? Run oh-my-cli --browse-sessions in
a terminal to search and arrow through your sessions, then resume the selected
one. It resumes the exact session and restores its declared workspace, and it
fails closed with an actionable message rather than resuming something else when
the chosen session
暂无开放 Issues,或尚未同步最近议题。