Metadata-Version: 2.5
Name: insightfactory-cli
Version: 1.0.1
Summary: Profile-based authentication CLI for the InsightFactory Interfaces API
Project-URL: Homepage, https://github.com/insightfactory-ai/insightfactory-cli
Project-URL: Repository, https://github.com/insightfactory-ai/insightfactory-cli
Author-email: "insightfactory.ai Support" <support@insightfactory.ai>
Keywords: cli,insightfactory,oauth,pkce
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# insightfactory-cli

Profile-based authentication CLI for the InsightFactory Interfaces API. It
installs the `if-cli` command: browser OAuth login (authorization-code + PKCE),
token refresh, and authenticated API calls, with profiles stored in
`~/.insightfactory` in the same layout as the Node `@insightfactory-ai/if-cli`
so the two implementations can share a config directory.

This is the production Python CLI, published to public PyPI. Python 3.10 or
newer is required.

## Install

```bash
uv tool install insightfactory-cli   # persistent install, on PATH
uvx insightfactory-cli profiles      # or zero-install, run-once
uv tool upgrade insightfactory-cli   # updates
```

`uv` fetches a managed Python automatically if the machine lacks one. Fallbacks:

```bash
pipx install insightfactory-cli
pip install insightfactory-cli
```

Develop-branch builds are published as PEP 440 `.devN` pre-releases. Install the
bleeding edge with:

```bash
pip install --pre insightfactory-cli
uv tool install --prerelease allow insightfactory-cli
```

PEP 440 orders `1.0.0.devN` *before* `1.0.0`, so a dependency spec of
`insightfactory-cli>=1.0.0` does **not** match a develop-channel dev build.
An in-process consumer that wants those builds must use a pre-release-bearing
spec such as `insightfactory-cli>=1.0.0.dev0`, which matches both `.devN`
builds and the final `1.0.0` release.

## Usage

```bash
# Create a profile and authenticate through the browser.
if-cli login -p example-dev --host https://factory.example

# Inspect profiles and cached-token status. Statuses are colour-coded on a TTY
# (green valid, yellow refreshable-but-expired, red missing or broken); set
# NO_COLOR=1 to disable or FORCE_COLOR=1 to keep colour when piping.
if-cli profiles
if-cli profiles --json

# Read a single non-secret profile value for scripting.
if-cli config get host -p example-dev

# Print a valid token, refreshing it when a refresh token is available.
if-cli token -p example-dev
if-cli token -p example-dev --env
if-cli token -p example-dev --env-name INSIGHTFACTORY_ACCESS_TOKEN_DEV

# Make an authenticated API request.
if-cli api -p example-dev /api/agent-projects
if-cli api -p example-dev -X POST -d '{"name":"example"}' /api/example

# Discover API routes from the factory's OpenAPI document.
if-cli api routes -p example-dev
if-cli api routes -p example-dev agent-projects
if-cli api describe -p example-dev GET /api/agent-projects/{id}

# Remove the cached credential for a profile.
if-cli logout -p example-dev

# Fallback for factories where browser OAuth is not available.
if-cli set-token -p example-dev

if-cli --version
```

Profile selection uses `-p`, then `INSIGHTFACTORY_CONFIG_PROFILE`, then
`[DEFAULT]`. Route discovery reads `{host}/swagger/v1/swagger.json` and does
not require a login. Authenticated API requests are restricted to the selected
factory origin so a profile token cannot be forwarded to another host. Profile
hosts must be bare origins rather than URLs containing application path
prefixes.

Override the config directory with `INSIGHTFACTORY_CONFIG_DIR` (useful in tests
and CI). The default is `~/.insightfactory`.

On Git Bash (MSYS2) on Windows, path conversion rewrites a leading-slash
argument such as `/api/agent-projects` into a Windows path. Prefix the command
with `MSYS_NO_PATHCONV=1`:

```bash
MSYS_NO_PATHCONV=1 if-cli api -p example-dev /api/agent-projects
```

### Scripting against profiles

`profiles --json` and `config get` are the supported machine-readable
interfaces; the padded `profiles` columns are for humans and are not a
contract, and neither is the layout of `~/.insightfactory/config`.

`if-cli profiles --json` prints an array — one entry per configured profile, in
file order — of `name`, `host`, and `token`:

```json
[
  {
    "name": "example-dev",
    "host": "https://factory.example",
    "token": { "status": "valid", "expires_at": 1785918336, "refreshable": true }
  },
  { "name": "other", "host": "https://other.factory.example", "token": { "status": "none" } }
]
```

`token.status` is `valid`, `expired`, `none`, or `unknown`; `expires_at` and
`refreshable` are present only when a cached token exists. `valid` means
`if-cli token` will hand back the cached token as-is, so a token inside the
60-second slack window that command refreshes within is reported as `expired` —
use the raw `expires_at` if you need the literal expiry instant instead. A
profile that cannot be resolved has a `host` of `null` (or its raw configured
value) and a top-level `error` describing the problem, and does not stop the
other profiles from being listed; `token.error` is set only when the token
cache itself could not be read.

`if-cli config get <key> -p <profile>` prints one newline-terminated value and
exits non-zero if the profile cannot be resolved, or the key is not configured
within it. A profile with no host cannot be resolved at all, so every key on it
fails, not just `host`. A key present but empty (`client_id =`) counts as not
configured:

```bash
curl -H "Authorization: Bearer $(if-cli token -p example-dev)" \
  "$(if-cli config get host -p example-dev)/api/agent-projects"
```

The readable keys are `host`, `audience`, `callback_port`, `client_id`, and
`organization` — all non-secret, and resolved the same way the CLI itself
resolves them, so `audience` and `callback_port` return their defaults when the
profile omits them. Tokens are deliberately not readable this way; use
`if-cli token`.

## Programmatic API

`insightfactory-cli` may be imported in-process as a library to resolve a
profile to a factory host and a fresh access token. These four symbols are a
supported programmatic surface for in-process consumers:

- `if_cli.config.load_config`
- `if_cli.config.get_profile`
- `if_cli.oauth.get_valid_token`
- `if_cli.runtime.CliError`

They raise `CliError` on failure (never `SystemExit` or `sys.exit`), never
write to stdout, never launch a browser or block on interactive input, and use
only bounded waits. Changing or removing any of them is a breaking change and
requires a major version bump.

## How authentication works

- Profiles are stored in `~/.insightfactory/config`, one per customer/environment.
- The profile file is managed by the CLI and is regenerated on updates; comments
  and empty sections are not preserved.
- Tokens are cached by host in `~/.insightfactory/token-cache.json` with file
  mode `0600`.
- The configuration directory is `0700`; configuration and cache updates are
  atomic.
- Remote factory, authorization, and token endpoints must use HTTPS. HTTP is
  accepted only for loopback development.
- Login discovers OAuth metadata from
  `{host}/.well-known/oauth-authorization-server`.
- Authorization uses PKCE through the factory `/authorize` proxy with a
  loopback-only (`127.0.0.1`) callback. The CLI is a public OAuth client: the
  client ID is read from the factory's discovery document at runtime (no client
  secret).

## Development

```bash
uv sync
uv run pytest
uv run ruff check .
uv run ty check
```

Smoke-test against a throwaway config directory, never `~/.insightfactory`:

```bash
uv run if-cli --help
uv run if-cli --version
INSIGHTFACTORY_CONFIG_DIR=$(mktemp -d) uv run if-cli profiles
```

Python 3.10 or newer is required. Runtime code is stdlib-only.

## Release

**A release is cut by pushing a tag, not by merging.** Merging to `main` runs CI and
publishes nothing.

```bash
git tag v1.0.1 && git push origin v1.0.1
```

Verify and build live in the reusable workflow
[`if_s_insightfactory_cli.yml`](https://github.com/insightfactory-ai/if_sre_github_actions/blob/main/.github/workflows/if_s_insightfactory_cli.yml)
in `insightfactory-ai/if_sre_github_actions`. This repository's
`.github/workflows/release.yml` keeps only the OIDC publish job, because PyPI
Trusted Publishing is registered for **this** repo, workflow `release.yml`, and
environment `pypi` — a reusable workflow cannot be registered.

A one-time trusted-publisher registration is required on the PyPI project for
that triple. The publish job downloads the `dist-release` artifact staged by
the reusable workflow and uploads it with `pypa/gh-action-pypi-publish`.

| Trigger | Published version | PyPI role |
|---|---|---|
| Push / merge to `develop` | `1.0.1.dev{N}` | pre-release (`pip install --pre`) |
| Push of a `v*` tag | the version in `pyproject.toml`, which the tag must match | latest |
| Merge to `main` | nothing — CI only | — |

`N` is `github.run_number` of the reusable workflow: a per-workflow integer
that goes up on every run and stays the same across re-runs of that run (so a
failed publish can be retried under the same version). It does not depend on
git history or clone depth. After a successful release, bump `version` in
`pyproject.toml` on `develop` so later `.devN` builds sort *after* what just
landed; PEP 440 puts `1.0.1.devN` before `1.0.1`. Because of that ordering, a
dependency spec of `insightfactory-cli>=1.0.0` does not match develop-channel
`.devN` builds; an in-process consumer that wants them must specify a
pre-release-bearing spec such as `insightfactory-cli>=1.0.0.dev0` (which also
matches the final `1.0.0` release).

A `v*` tag whose name does not match `pyproject.toml` fails before publishing.

The two channels treat an already-published version differently, on purpose:

- **Release channel (a tag):** publishing fails, naming the fix. A tag is a request to
  release that version; if it cannot be honoured, the run must say so rather than report
  success having published nothing. (git also refuses to push a tag that already exists,
  so this is hard to reach.)
- **Dev channel (`develop`):** publishing is skipped quietly. `.dev{N}` uses
  `github.run_number`, which is stable across re-runs of one run, so an already-present
  version there can only mean a re-run of a run that already published — real idempotency,
  not a swallowed mistake.

This split is why releasing moved off `main`. While a merge was the trigger, an
already-published version *had* to be tolerated as a no-op for merges to stay green — so a
merge that released nothing looked exactly like one that released, and a forgotten version
bump shipped nothing silently.

## Differences from the Node CLI

Behaviour matches `@insightfactory-ai/if-cli` except where the platforms
genuinely diverge:

- **Unknown-option wording** is aligned (`Unknown option '--flag'`). Other
  argparse messages (for example a missing option value) still differ from
  Node's `util.parseArgs`.
- **Expiry timestamps** in human-readable output use the C-locale `%c` format
  rather than JavaScript `Date#toLocaleString()`.
- **`--version`** reads the installed package metadata (`1.0.0` and later)
  rather than an npm `package.json`.

On-disk profile and token-cache paths, JSON shapes, and file permissions stay
compatible with the Node CLI, so you can switch implementations without
re-authenticating.
