Unified LLM Interface Specification — one config source for AI tools.
ulis is a CLI that helps you write your agent configuration once, then publish it to:
Claude Code, OpenCode, Codex, Cursor, and ForgeCode.
📖 Docs: nejcm.github.io/ulis
Instead of maintaining separate "dialects" per platform, you keep a single canonical tree in .ulis/ (per project) or ~/.ulis/ (global). Running ulis generates the native files each platform expects and installs them into the right locations.
npm i -g @nejcm/ulisor
bun add -g @nejcm/ulisRequires Node 20+. Works with both Node and Bun runtimes.
@nejcm/ulis also ships reusable skills under skills/.
Install the ulis guide skill directly:
npx skills@latest add @nejcm/ulis --skill ulisOr install it through your ULIS source tree:
"*":
skills:
- name: "@nejcm/ulis"
args:
- --skill
- ulisThat entry belongs in .ulis/skills.yaml, then ulis install will forward it to the skills CLI for supported targets.
Scaffold a .ulis/ folder inside an existing project:
cd my-project
ulis initThis creates:
.ulis/
├── config.yaml # version + project name
├── mcp.yaml # MCP server definitions
├── permissions.yaml # per-platform access rules
├── skills.yaml # external skill installs (per platform)
├── extensions.yaml # third-party CLI extension installs (per platform)
├── agents/ # agent definitions (.md with frontmatter)
├── skills/ # skill definitions (SKILL.md per skill)
├── commands/ # slash commands
└── raw/ # platform-specific fragments copied verbatimIt also appends /.ulis/generated/ to .gitignore.
Add some agents/skills/MCP servers, then:
ulis installThis builds into .ulis/generated/<platform>/ and then deploys to ./.claude/, ./.codex/, ./.cursor/, ./.opencode/, and ForgeCode locations (./.forge/) inside your project. Pass -y / --yes to skip confirmation prompts.
Maintain one canonical config for every project on your machine:
ulis init --global # creates ~/.ulis/
# edit ~/.ulis/... to taste
ulis install --global # deploys to ~/.claude/, ~/.codex/, ~/.cursor/, ~/.config/opencode/, ~/.forge/These patterns are independent; you can use project mode in some repos, global mode on the same machine, and presets whenever you want shared layers on top of a base source.
Use this when agents, skills, or MCP servers should be specific to one codebase (or when your team wants the same setup for everyone who clones the repo).
- Run
ulis initat the repository root. That creates./.ulis/beside your code. - Edit YAML and add agents under
.ulis/agents/, skills under.ulis/skills/, and so on. - Run
ulis install(orulis install --yesin scripts). Outputs land in./.claude/,./.cursor/, and the other tool folders inside the project.
You can commit .ulis/ so the team shares one source of truth, and add .ulis/generated/ (and optionally the generated tool dirs) to .gitignore if you prefer each developer to regenerate locally.
Use this when you want one personal baseline that applies everywhere you work, without copying config into every repo.
- Run
ulis init --globalonce. That creates~/.ulis/in your home directory. - Maintain the same kind of tree as in project mode (
config.yaml,mcp.yaml,agents/, …). - Run
ulis install --global. Outputs go to~/.claude/,~/.cursor/, and the other home-level tool directories.
From any directory, ulis build --global / ulis install --global uses ~/.ulis/ as the source. To build a different tree but still install to home (for example a fork of your config in another path), use ulis install --global --source /path/to/that/.ulis.
Presets are mini .ulis/-shaped trees you apply before your base source on a given run. They are useful for stacks you reuse often (“our React defaults”, “backend service template”) without duplicating files in every repo.
- User presets live under
~/.ulis/presets/<name>/. Each preset can include the same files and folders as a full source (agents/,mcp.yaml, …). Optionalpreset.yamlholds metadata (name,description) forulis preset list. - Bundled presets ship with the CLI (run
ulis preset listto see names such asreact-web,backend,golang-backend). If a name exists in both places, your~/.ulis/presets/copy wins.
Merge order is: first preset, second preset, …, then your base source (project ./.ulis/ or ~/.ulis/). When the same agent or key appears in a preset and the base, the base wins.
ulis preset list
ulis install --preset react-web --yes # base = ./.ulis/ in cwd
ulis install --global --preset backend --yes # base = ~/.ulis/
ulis build --preset golang-backend,node-backend # multiple presets, left to rightIn CI or other non-interactive runs, add --yes so a missing preset name fails immediately instead of prompting.
| Command | Purpose |
|---|---|
ulis init |
Scaffold .ulis/ in the current project (or ~/.ulis/ with --global) |
ulis build |
Generate configs into <source>/generated/ without installing |
ulis install |
Build, then deploy generated configs to the target platform directories |
ulis preset |
List available presets from ~/.ulis/presets/ |
ulis tui |
Launch the interactive dashboard for source, presets, build, and install |
| Flag | Applies to | Description |
|---|---|---|
-g, --global |
all | Operate on ~/.ulis/ and home-level install targets (~/.claude/…) |
--source <path> |
build, install |
Override the source directory; with --global, installs still target home |
--target <platform> |
build, install |
Comma-separated list: claude, codex, cursor, opencode, forgecode |
--preset <names> |
build, install |
Apply preset(s) from ~/.ulis/presets/ or bundled presets (comma-separated) |
-y, --yes |
install |
Skip confirmation prompts |
--no-rebuild |
install |
Skip the build step and deploy existing generated/ |
--backup |
install |
Back up existing platform dirs (<dir>.backup.YYYYMMDD_HHMMSS) |
--runner <name> |
install |
Package runner for extensions.yaml (npx or bunx) |
--no-extensions |
install |
Skip running entries from extensions.yaml |
See Common workflows → Presets as shared layers for a fuller explanation. Summary: presets merge before the base source (user ~/.ulis/presets/<name>/ overrides bundled names). Use ulis preset list to discover names.
ulis build --preset team-default
ulis install --preset team-default,react-web --yesWhen --yes is set, missing presets fail fast with an error instead of prompting, which keeps CI runs non-interactive and deterministic.
ulis build / ulis install pick the source directory in this order:
--source <path>if provided — fails if missing.--global→~/.ulis/— fails with anulis init --globalhint if missing.- Otherwise →
./.ulis/in the current directory (no walk-up) — fails with anulis inithint if missing.
For ulis install --source <path> --global, the explicit source is built, then files are installed to home-level targets.
Your .ulis/ tree is the single source of truth that ulis reads when you run ulis build / ulis install.
You can define:
config.yaml– project identitymcp.yaml– MCP servers shared across platformspermissions.yaml– per-platform read/write/bash access rulesskills.yaml– external skill installsextensions.yaml– third-party CLI extension installs (run vianpx/bunx)agents/*.md– agents (prompt + frontmatter)skills/<name>/SKILL.md– skillscommands/andraw/– pass-through fragments copied into generated outputs
For the full field-level schema and examples, see docs/REFERENCE.md. For architecture, see docs/SPEC.md.
| Tool | Strategy | Target (project mode) | Target (global mode) |
|---|---|---|---|
| OpenCode | Overwrite | ./.opencode/ |
~/.config/opencode/ (%USERPROFILE%\.config\opencode\ on Windows) |
| Claude Code | Merge (additive) | ./.claude/ |
~/.claude/ |
| Codex | Overwrite | ./.codex/ |
~/.codex/ |
| Cursor | Merge (additive) | ./.cursor/ |
~/.cursor/ |
| ForgeCode | Merge (additive) | ./.forge/ |
~/.forge/ |
settings.json, .claude.json, mcp.json, and ForgeCode's .forge/.mcp.json are deep-merged so user content outside ulis-managed keys is preserved. With --backup, existing platform directories/files are copied aside before overwriting.
ulis install runs phases in this order: build → files → skills → extensions. Extensions run last because they typically mutate the same files ulis just deployed.
Third-party CLI extensions that wire themselves into a target tool — for example bunx codex-supermemory@latest install — are declared once in .ulis/extensions.yaml and re-run on every ulis install. Each entry runs through a package runner.
codex:
extensions:
- key: supermemory # optional, used in logs
name: codex-supermemory@latest
args: ["install"] # optional
claude:
extensions:
- name: some-claude-helper@1.2.3
args: ["setup", "--yes"]Runner resolution (highest precedence first):
--runner <npx|bunx>CLI flagrunner: npx | bunxinconfig.yaml- Auto-detect:
bunxif available on PATH, otherwisenpx
A single failing extension logs a warning and the install continues. Skip the phase entirely with --no-extensions. Extensions are always re-run on each install (no caching in this version — see SPEC.md for the future caching plan).
Scaffolded YAML files include a # yaml-language-server: $schema=… header pointing at ./node_modules/@nejcm/ulis/schemas/*.schema.json. VS Code's YAML extension picks these up automatically when the package is installed.
Schemas are also regenerated on every npm run build via bun run gen:schemas.
Want to build on ulis itself? These scripts help you run the CLI locally, regenerate generated assets, and verify changes with the test suite.
Clone the repo:
bun install
bun run dev # builds against example/
bun test # runs the suite (~96 tests)
bun run build # bundles dist/cli.js + regenerates dist/schemas + schemas/ (npm pack)| Script | Purpose |
|---|---|
bun run build |
Bundle CLI (tsup) and regenerate JSON schemas |
bun run dev |
Run ulis build against the example/ directory |
bun run ulis <args> |
Run the CLI from source (tsx src/cli.ts …) |
bun run tui |
Launch the interactive TUI from source |
bun run test |
Run the unit + integration suite |
bun run lint |
tsc --noEmit |
bun run format |
Format with oxfmt |
bun run gen:schemas |
Regenerate dist/schemas/*.schema.json and schemas/*.schema.json from Zod |
bun run gen:reference |
Regenerate docs/REFERENCE.md |
src/
cli.ts # cac entry point (compiled to dist/cli.js)
commands/ # init, install, build, tui
parsers/ # agent, skill, mcp, permissions loaders
generators/ # claude, opencode, codex, cursor, forgecode
schema/ # Zod schemas (ulis-config, agent, mcp, …)
scaffold/ # inline templates used by `ulis init`
tui/ # TUI dashboard state/actions/render modules
utils/ # config-loader, resolve-source, fs, logger, …
validators/ # cross-ref + collision checks
tui.ts # TUI entrypoint + effect runner
tools/ # gen-json-schema, gen-reference
example/ # reference example config
tests/
docs/
SPEC.md # architecture + entity model
REFERENCE.md # auto-generated field referenceThe bun run dev command builds against example/ so the CLI works without any .ulis/ in the current directory.
The bun run dev:install --source example command installs the example into global configs.
- docs/SPEC.md — architecture, entity model, capability matrix, versioning, extension guide.
- docs/REFERENCE.md — auto-generated field reference for every schema.
- docs/CLI.md — full CLI reference (commands, flags, exit codes, examples).
ISC
