Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ jobs:
shell: bash
run: ./dist/${{ matrix.bin }} --help

- name: init --help
shell: bash
run: ./dist/${{ matrix.bin }} init --help

- name: login --no-browser prints URL
shell: bash
run: |
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ Installs a standalone binary on `postinstall` (macOS arm64/x64, Linux x64, Windo

```bash
npm install -g olostep-cli
olostep --version
olostep init
```

`olostep init` is the recommended first step — it signs you in, installs the Olostep skills into your AI agents, and configures the MCP server, all in one command. (Run `olostep --version` to check the install.)

Or run without installing:

```bash
Expand All @@ -55,13 +57,19 @@ olostep --version

## Sign in

The fastest way:
The fastest way to set up everything — sign in, skills, and MCP server:

```bash
olostep init
```

To **just sign in** (no skills/MCP):

```bash
olostep login
```

This opens your browser, you click **Authorize**, and the CLI saves your key to a local credentials file. You're done.
Either one opens your browser; you click **Authorize**, and the CLI saves your key to a local credentials file.

```bash
olostep login # browser flow
Expand Down
5 changes: 3 additions & 2 deletions config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,9 @@ def resolve_api_key(api_key: str | None = None) -> str:
if cred:
return cred
raise ValueError(
f"Missing API key. Set {ENV_API_KEY} or {ENV_API_TOKEN}, add a project `.env`, "
f"or run `olostep login` (saves {CREDENTIALS_FILE_NAME} under the app config directory)."
"Not signed in. Run `olostep init` to sign in and set up skills + the "
f"MCP server, or `olostep login` to just sign in. You can also set "
f"{ENV_API_KEY} / {ENV_API_TOKEN} or add a project `.env`."
)


Expand Down
130 changes: 130 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,136 @@ def login_cmd(
)


def _is_authenticated() -> bool:
try:
return bool(resolve_api_key())
except (ValueError, SystemExit):
return False


@app.command("init")
def init_cmd(
no_browser: bool = typer.Option(
False, "--no-browser", help="Print the authorize URL instead of opening a browser."
),
skills_only: bool = typer.Option(
False, "--skills-only", help="Only install skills (skip the MCP server)."
),
mcp_only: bool = typer.Option(
False, "--mcp-only", help="Only install the MCP server (skip skills)."
),
relogin: bool = typer.Option(
False, "--relogin", help="Sign in again even if already authenticated."
),
):
"""Set up Olostep in one step: sign in, install skills, install the MCP server.

This is the recommended first command after `npm install -g olostep-cli`.

[bold]Examples:[/bold]

[dim]$[/dim] olostep init
[dim]$[/dim] olostep init --skills-only
[dim]$[/dim] olostep init --no-browser
"""
if skills_only and mcp_only:
raise typer.BadParameter("Use only one of --skills-only or --mcp-only.")

typer.secho("")
typer.secho(" Setting up Olostep…", bold=True)
typer.secho("")

# 1. Authenticate. Skills install without a key, so --skills-only skips
# login entirely. A failed login is non-fatal: skills still install,
# only the MCP server (which needs a key) is skipped.
authed = _is_authenticated()
if not skills_only and (relogin or not authed):
try:
_run_login_flow(poll_seconds=3.0, timeout_s=600.0, no_browser=no_browser, env_file=None)
authed = True
except typer.Exit:
typer.secho(
" — Sign-in didn't complete — installing skills only. "
"Run `olostep init` again or `olostep login` to add the MCP server.",
fg="yellow",
)
authed = False
elif authed and not skills_only:
typer.secho(" ✓ Already signed in", fg="green")

# 2. Install skills.
if not mcp_only:
opts = InstallOptions(
source=CLI_LOCAL_SKILLS_DIR,
cli_local_dir=CLI_LOCAL_SKILLS_DIR,
canonical_dir=DEFAULT_CANONICAL_DIR,
lockfile_path=DEFAULT_LOCKFILE,
agent=[],
all_agents=True,
global_install=True,
agent_skills_dir=None,
skill=[],
exclude=[],
dry_run=False,
overwrite=True,
link_mode="auto",
yes=True,
)
try:
sk = run_skills_install(opts)
n = len(sk["selected_skills"])
tgts = sk["targets"]
if tgts:
typer.secho(
f" ✓ Installed {n} skills across {len(tgts)} "
f"agent{'s' if len(tgts) != 1 else ''}: {', '.join(tgts)}",
fg="green",
)
else:
typer.secho(
f" ✓ Installed {n} skills (no AI agents detected yet)", fg="green"
)
except (ValueError, OSError) as exc:
typer.secho(f" — Skills install skipped: {exc}", fg="yellow")

# 3. Install the MCP server (needs an API key — skipped if sign-in failed).
if not skills_only and not authed:
typer.secho(
" — MCP server skipped (not signed in). Run `olostep init` again.",
fg="yellow",
)
elif not skills_only:
try:
key = resolve_api_key()
targets = resolve_mcp_targets(agents=[], all_agents=True)
if not targets:
typer.secho(
" — MCP server: no agents detected — run `olostep mcp install --agent <name>` later",
fg="yellow",
)
else:
run_mcp_install(
agents=targets, api_key=key, transport="http",
global_install=True, overwrite=True, dry_run=False,
)
typer.secho(
f" ✓ Olostep MCP server installed in {len(targets)} "
f"agent{'s' if len(targets) != 1 else ''}: {', '.join(targets)}",
fg="green",
)
except (ValueError, OSError, SystemExit) as exc:
typer.secho(f" — MCP install skipped: {exc}", fg="yellow")

typer.secho("")
typer.secho(" You're ready. Restart your AI agent, then try:", dim=True)
typer.secho(' olostep scrape https://example.com')
typer.secho(' olostep answer "what is olostep"')
typer.secho("")
typer.secho(" olostep status # see what's set up", dim=True)
typer.secho(" olostep --help # all commands", dim=True)
typer.secho("")


@app.command("status")
def status_cmd(
as_json: bool = typer.Option(False, "--json", help="Machine-readable JSON output."),
Expand Down
8 changes: 5 additions & 3 deletions npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ Installing this package downloads a **standalone binary** for your OS. You do no

```bash
npm install -g olostep-cli
olostep --version
olostep init
```

`olostep init` is the recommended first step — it signs you in, installs the Olostep skills into your AI agents, and configures the MCP server in one command.

Run without installing:

```bash
Expand All @@ -29,13 +31,13 @@ On install, a postinstall step downloads the matching binary from the package's

## Sign in

The fastest way:
`olostep init` sets up everything (sign in + skills + MCP server). To **just sign in**:

```bash
olostep login
```

This opens your browser, you click **Authorize**, and the CLI saves your key locally. You're done.
This opens your browser, you click **Authorize**, and the CLI saves your key locally.

```bash
olostep login --no-browser # print the URL (useful over SSH)
Expand Down
8 changes: 8 additions & 0 deletions npm/scripts/postinstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ async function main() {
}

console.log("olostep binary installed successfully.");
console.log("");
console.log(" Next step — set up Olostep in one command:");
console.log("");
console.log(" olostep init");
console.log("");
console.log(" It signs you in, installs the Olostep skills into your AI");
console.log(" agents, and configures the MCP server. Or run `olostep --help`.");
console.log("");
}

main().catch((err) => {
Expand Down
84 changes: 84 additions & 0 deletions tests/unit/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from unittest.mock import AsyncMock, patch

import pytest
import typer
from typer.testing import CliRunner

from main import app
Expand Down Expand Up @@ -565,3 +566,86 @@ def test_update_registry_unreachable(self):
result = runner.invoke(app, ["update", "--check"])
assert result.exit_code == 0
assert "Couldn't reach" in result.output


class TestInitCommand:
_fake_skills = {"selected_skills": ["scrape", "answers"], "targets": ["cursor"]}

def test_init_runs_login_skills_and_mcp(self):
with patch("main._is_authenticated", return_value=False), \
patch("main._run_login_flow") as login, \
patch("main.run_skills_install", return_value=self._fake_skills) as skills, \
patch("main.resolve_mcp_targets", return_value=["cursor"]), \
patch("main.resolve_api_key", return_value="k"), \
patch("main.run_mcp_install", return_value=[]) as mcp:
result = runner.invoke(app, ["init"])
assert result.exit_code == 0
login.assert_called_once()
skills.assert_called_once()
mcp.assert_called_once()
assert "Setting up Olostep" in result.output
# Skills install must target all detected agents, globally.
opts = skills.call_args.args[0]
assert opts.all_agents is True and opts.global_install is True
# MCP install must use the hosted transport.
assert mcp.call_args.kwargs["transport"] == "http"

def test_init_skills_only_does_not_log_in(self):
"""--skills-only needs no API key, so it must not trigger login."""
with patch("main._is_authenticated", return_value=False), \
patch("main._run_login_flow") as login, \
patch("main.run_skills_install", return_value=self._fake_skills) as skills, \
patch("main.run_mcp_install", return_value=[]) as mcp:
result = runner.invoke(app, ["init", "--skills-only"])
assert result.exit_code == 0
login.assert_not_called()
skills.assert_called_once()
mcp.assert_not_called()

def test_init_continues_when_login_fails(self):
"""A failed sign-in must not abort init — skills still install, MCP is skipped."""
with patch("main._is_authenticated", return_value=False), \
patch("main._run_login_flow", side_effect=typer.Exit(code=1)), \
patch("main.run_skills_install", return_value=self._fake_skills) as skills, \
patch("main.run_mcp_install", return_value=[]) as mcp:
result = runner.invoke(app, ["init"])
assert result.exit_code == 0
skills.assert_called_once() # skills still installed
mcp.assert_not_called() # MCP skipped (no key)
assert "Sign-in didn't complete" in result.output

def test_init_skips_login_when_authenticated(self):
with patch("main._is_authenticated", return_value=True), \
patch("main._run_login_flow") as login, \
patch("main.run_skills_install", return_value=self._fake_skills), \
patch("main.resolve_mcp_targets", return_value=[]), \
patch("main.resolve_api_key", return_value="k"), \
patch("main.run_mcp_install", return_value=[]):
result = runner.invoke(app, ["init"])
assert result.exit_code == 0
login.assert_not_called()
assert "Already signed in" in result.output

def test_init_skills_only_skips_mcp(self):
with patch("main._is_authenticated", return_value=True), \
patch("main.run_skills_install", return_value=self._fake_skills) as skills, \
patch("main.run_mcp_install", return_value=[]) as mcp:
result = runner.invoke(app, ["init", "--skills-only"])
assert result.exit_code == 0
skills.assert_called_once()
mcp.assert_not_called()

def test_init_mcp_only_skips_skills(self):
with patch("main._is_authenticated", return_value=True), \
patch("main.run_skills_install", return_value=self._fake_skills) as skills, \
patch("main.resolve_mcp_targets", return_value=["cursor"]), \
patch("main.resolve_api_key", return_value="k"), \
patch("main.run_mcp_install", return_value=[]) as mcp:
result = runner.invoke(app, ["init", "--mcp-only"])
assert result.exit_code == 0
skills.assert_not_called()
mcp.assert_called_once()

def test_init_rejects_both_only_flags(self):
result = runner.invoke(app, ["init", "--skills-only", "--mcp-only"])
assert result.exit_code != 0
2 changes: 1 addition & 1 deletion tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def test_missing_key_raises(self, monkeypatch, tmp_path):
old_tok = os.environ.pop("OLOSTEP_API_TOKEN", None)
monkeypatch.setattr("config.config.get_credentials_path", lambda: tmp_path / "none.json")
try:
with pytest.raises(ValueError, match="Missing API key"):
with pytest.raises(ValueError, match="Not signed in"):
resolve_api_key()
finally:
if old_key:
Expand Down
Loading