Metadata-Version: 2.5
Name: insightfactory-cli
Version: 1.0.2
Summary: Profile-based authentication CLI for the InsightFactory Interfaces API
Project-URL: Homepage, https://insightfactory.ai
Author-email: "insightfactory.ai Support" <support@insightfactory.ai>
License-Expression: LicenseRef-Proprietary
License-File: LICENSE
Keywords: cli,insightfactory,oauth,pkce
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
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

# Wait longer than the 30-second default for a long-running endpoint.
if-cli api -p example-dev --timeout 300 -X PUT -d '{"productionLineCodes":["PL001"]}' \
  /api/orchestration/run

# 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.

Requests time out after 30 seconds by default. Raise the deadline per
invocation with `--timeout <seconds>`, or for a whole session with
`INSIGHTFACTORY_REQUEST_TIMEOUT`; the flag wins over the environment variable,
and both are validated before the command touches the network. There is no
upper bound, because the right ceiling depends on the endpoint.

Both apply to the API request itself. The OAuth discovery and token-refresh
round trips keep the 30-second default, so `if-cli token` and in-process callers
of `if_cli.oauth.get_valid_token` always have a bounded wait that no ambient
environment value can redefine.

A timeout is a client-side deadline, not a rejection: the factory may have
accepted and completed the request after `if-cli` gave up. Re-issuing a
non-idempotent call (`POST`, `PUT`, `PATCH`, `DELETE`) can therefore apply it
twice, so the timeout message for those methods says to confirm the current
state before retrying rather than repeating the call.

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

The request method goes in `-X`; `if-cli api` takes exactly one positional
argument and reads it as the path. A `curl`-shaped `if-cli api GET /api/schedules`
therefore parses `GET` as the path — write `if-cli api -X GET /api/schedules`
instead. The CLI names the misplaced verb rather than reporting a problem with
`/api/schedules`.

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.

## Licence

Copyright 2026 insightfactory.ai. All rights reserved. This package is proprietary
and may be used only under a separate written agreement with insightfactory.ai.
