insightfactory-cli
Profile-based authentication CLI for the InsightFactory Interfaces API. It
installs the if-cli command, which logs in through the browser with OAuth
authorization code and PKCE, refreshes tokens, and makes authenticated API
calls. Profiles live in ~/.insightfactory in the same layout the Node
@insightfactory-ai/if-cli uses, so both implementations can share one config
directory.
This is the production Python CLI, published to public PyPI. It needs Python 3.10 or newer.
Install
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 if the machine lacks one. Fallbacks:
pipx install insightfactory-cli
pip install insightfactory-cli
Develop-branch builds are published as PEP 440 .devN pre-releases. Install
the latest one with:
pip install --pre insightfactory-cli
uv tool install --prerelease allow insightfactory-cli
PEP 440 sorts 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 needs a spec that admits
pre-releases, such as insightfactory-cli>=1.0.0.dev0. That spec matches both
the .devN builds and the final 1.0.0 release.
Usage
# 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 tries -p first, then INSIGHTFACTORY_CONFIG_PROFILE, then
[DEFAULT]. Route discovery reads {host}/swagger/v1/swagger.json and does
not need a login. Authenticated requests only go to the selected factory's
origin, so a profile token cannot be forwarded to another host. A profile host
must be a bare origin, not a URL with an application path prefix.
Requests time out after 30 seconds by default. Raise the deadline for one
invocation with --timeout <seconds>, or for a whole session with
INSIGHTFACTORY_REQUEST_TIMEOUT. The flag wins over the environment variable,
and the CLI validates both before it touches the network. There is no upper
bound, because the right ceiling depends on the endpoint.
Both settings apply to the API request itself. OAuth discovery and token
refresh keep the 30-second default, so if-cli token and in-process callers of
if_cli.oauth.get_valid_token always wait a bounded time that no ambient
environment value can change.
A timeout is a client-side deadline, not a rejection. The factory may have
accepted and completed the request after if-cli gave up, so re-issuing a
POST, PUT, PATCH, or DELETE can apply it twice. For those methods the
timeout message tells you to confirm the current state before retrying rather
than repeat the call.
Override the config directory with INSIGHTFACTORY_CONFIG_DIR, which is 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, so the curl-shaped
if-cli api GET /api/schedules parses GET as the path. Write
if-cli api -X GET /api/schedules instead. The error 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:
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 with one entry per configured profile,
in file order, each holding name, host, and token:
[
{
"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 appear only when a cached token exists. valid means
if-cli token will hand back the cached token as-is. A token inside the
60-second slack window that command refreshes within is therefore reported as
expired, so read the raw expires_at if you need the literal expiry instant.
A profile that cannot be resolved has a host of null, or its raw configured
value, plus a top-level error describing the problem. It 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
in it. A profile with no host cannot be resolved at all, so every key on it
fails, not just host. A key that is present but empty, such as client_id =,
counts as not configured:
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 are non-secret and resolved the same way the CLI 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
You can import insightfactory-cli in-process to resolve a profile to a
factory host and a fresh access token. These four symbols are supported for
in-process consumers:
if_cli.config.load_configif_cli.config.get_profileif_cli.oauth.get_valid_tokenif_cli.runtime.CliError
They raise CliError on failure, never SystemExit or sys.exit. They never
write to stdout, never launch a browser or block on interactive input, and only
use bounded waits. Changing or removing any of them is a breaking change and
needs a major version bump.
get_valid_token takes a keyword-only force_refresh (default False). When
True, it refreshes the cached token even if it is still fresh, which a
caller that just received a 401 from the factory needs: the cached token was
fresh a moment ago but the factory has since rejected it. With a refresh
token cached, this re-mints regardless of freshness; without one — a token
pasted through if-cli set-token — it raises CliError with the same
if-cli login -p <name> hint rather than serving the token that was just
rejected.
How authentication works
- Profiles live in
~/.insightfactory/config, one per customer and environment. - The CLI manages the profile file and regenerates it on updates. Comments and empty sections are not preserved.
- Tokens are cached by host in
~/.insightfactory/token-cache.jsonwith file mode0600. - 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
/authorizeproxy with a loopback-only127.0.0.1callback. The CLI is a public OAuth client. It reads the client ID from the factory's discovery document at runtime and holds no client secret.
MCP router (if-cli mcp)
Runs one MCP server over stdio that fronts several factory environments as a
single tool catalogue, instead of one MCP connection per environment. Every
tool gets a required environment argument and each call is forwarded to
that environment's own factory /mcp endpoint, authenticated with that
environment's if-cli profile — a profile's bearer token never reaches a
different environment's host. A tool is only offered on the environments
whose input schema matches the published one, so a factory on an older
release cannot be handed argument shapes it does not accept.
It needs the mcp extra, which is not part of the base install. Name its environments once
in a [factory <name>] section of ~/.insightfactory/config, alongside profiles:
[factory foundry]
dev = example-dev
tst = example-tst
writable = dev
Every key except writable, catalog and skills is <environment code> = <profile name>;
file order is the environment order. writable is a comma-separated list of codes (default
none); catalog is one code (default the first environment); skills is true or false
(default false). Then:
uv tool install 'insightfactory-cli[mcp]'
uvx --from 'insightfactory-cli[mcp]' if-cli mcp foundry
The first uvx launch resolves and downloads the package from PyPI before it runs, which
added roughly four to six seconds on top of the usual startup in local testing, against
under two seconds once uv's cache was warm. A client with a fixed handshake budget
(Claude Code allows 30 seconds) usually has room for that, but a slow network can close
the gap. Two ways to avoid paying it on every launch: uv tool install 'insightfactory-cli[mcp]'
puts a real script on PATH with no resolve step, or run the same uvx --from ... command
by hand once before pointing a client at it, which warms uv's cache for the launches after.
Add it to .mcp.json:
{
"mcpServers": {
"if-factory": {
"command": "uvx",
"args": ["--from", "insightfactory-cli[mcp]", "if-cli", "mcp", "foundry"]
}
}
}
--env, --writable, --catalog and --allow-tool still work without a factory section
(--env is required in that case), and each one overrides the section when both are given:
a repeated --env CODE=PROFILE replaces that code's profile, --writable/--catalog, when
passed at all, replace the section's value outright, and --allow-tool is always additive.
| Flag | Meaning |
|---|---|
FACTORY (optional, first positional) |
Name of a [factory <name>] config section supplying the flags below. |
--env CODE=PROFILE (repeatable, required without FACTORY) |
The environment code the model passes, mapped to the if-cli profile that reaches it. |
--writable CODE (repeatable) |
Environments where write tools are allowed. Everything else is read-only. |
--read-only |
Forces every environment read-only, overriding both the section's writable and any --writable. Cannot be combined with --writable. |
--catalog CODE (default: the first environment) |
Whose tool list is published; its schemas are the reference every environment is checked against. |
--allow-tool NAME (repeatable) |
Extra tool names treated as read-only, beyond the shipped list. |
--skills / --no-skills |
Whether to advertise the experimental skills extension. Off unless the section says skills = true or --skills is passed. The two cannot be combined. |
--timeout SECONDS (default 120) |
Per upstream request; higher than the api command's default because some tools are slow. |
mcp's --timeout does not read INSIGHTFACTORY_REQUEST_TIMEOUT: it parses the flag with
parse_timeout_seconds rather than resolve_request_timeout, because a long-running stdio
server is not the one-shot request that environment variable is scoped to.
Only environments passed to --writable accept mutating tools; every other
environment allows only tools it knows to read (an allowlist, not a
blocklist — a tool the model reaches through invoke_tool is never in the
published catalogue, so a blocklist could never cover everything callable).
Passing dev-only writes to production means changing an argument value, not
connecting to a different server, so keep --writable scoped to the
environments meant to be written by hand.
Resources, prompts and completion
Resources and prompts are not merged across environments the way tools are, because a factory's non-tool content splits in two.
skill:// bodies are documentation that belongs to a release, identical on every
environment running it, and they cross-reference each other by bare URI. So they are
served from the --catalog environment exactly as published. Rewriting those URIs to
carry an environment would leave every link inside them pointing at nothing.
A resource template is the other half: if://task-config-schemas/{taskType} resolves
to one factory's own activity catalogue, not to a shared document. The router
republishes each template with its own variable appended, so the client chooses the
environment when it expands the URI:
if://task-config-schemas/{taskType}{?environment}
A template that already opens a query gets {&environment} instead, and one that
ends in a fragment gets the variable in front of it, because a ? after a # is an
ordinary character rather than a query. A template that already declares an
environment variable is skipped with a line on stderr, so one nonconforming item
does not hide the rest of the list.
Reading a URI with no ?environment= is answered by the catalogue environment,
unless the URI could be an expansion of a published template, which is refused
naming the codes to choose from. Membership of resources/list is deliberately not
the test: a skill body may link to a sibling document the factory never advertised,
and such a link carries no environment and could not be given one.
completion/complete reads the environment from the request's context arguments,
then from the reference URI, and falls back to the catalogue environment for anything
it does not recognise, because a half-typed context value is the normal state of a
completion request and completion should degrade rather than fail. The router answers
for the environment variable itself rather than asking a factory about an argument
it has never heard of.
Prompts gain a required environment argument like tools. The router also prepends a
line to every rendered prompt naming the environment chosen, because a factory writes
its prompts for a client talking to one factory and they tell the model to call tools
without one. Prepended rather than appended, and marked [if-cli router]: the
factory's own last turn may be an assistant prefill, which a trailing user turn would
silently end.
--skills only controls the advertisement. The handshake is answered before any
factory has been reached, so the router cannot ask the catalogue environment whether
it serves skills in time to repeat the claim, and an experimental capability is not
worth guessing at. The skill:// resources are served either way.
The advertisement reaches only a client on MCP protocol 2026-07-28 or later.
capabilities.extensions was added in that revision, and the initialize handshake
every current client opens with negotiates a 2025 revision whose capability schema
has no such field, so the SDK strips it before it reaches the wire. Against
foundryaz-dev with --skills on, Claude Code sees no extension and still finds
every skill through resources/list, which is how clients discover them today.
A tool the factory will not run until someone finishes a step in a browser, such
as linking a personal Databricks identity, comes back as a failed call naming the
page to visit rather than as a prompt. The router never relays the factory's
request for input to the model, and it names a page only on that environment's
own origin, logging a WARN line on stderr when it drops one that is not.
Development
uv sync
uv run pytest
uv run ruff check .
uv run ty check
Smoke-test against a throwaway config directory, never ~/.insightfactory:
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. The base install has no runtime dependencies;
optional features, such as if-cli mcp, ship as extras and are imported lazily
inside their command module.
Release
A release is cut by pushing a tag, not by merging. Merging to main runs CI
and publishes nothing.
git tag v1.0.1 && git push origin v1.0.1
The tag drafts the GitHub Release, uploads to PyPI, then publishes the Release.
Everything comes from one build, so the wheel and sdist attached to the Release
are the files that went to PyPI, and the draft means nobody sees pip install insightfactory-cli==1.1.1 on a Release page before that version exists on PyPI.
A failed upload leaves a draft rather than a published page pointing at a 404.
The body comes from CHANGELOG.md via tools/release_notes.py.
It covers the tagged version and any version opened since the last tag, because
one version per PR means unreleased versions pile up between tags: if 1.1.1 and
1.1.2 both merge and only v1.1.2 is tagged, 1.1.1's code ships inside that
artifact, so its notes belong on that Release. A checkout with no tags falls back
to the tagged version's section alone.
develop therefore carries the version the next release will use, and the
release-guard job holds it there. tools/check_release.py runs the checks and
is what fails the PR:
BASE=origin/develop RELEASED=origin/main python3 tools/check_release.py
Every PR into develop adds a CHANGELOG.md entry. That is the whole point
of the file, and a change with no entry reaches users with no line anywhere
saying it happened.
The version moves once per release cycle, not once per PR. The first PR after
a release raises version in pyproject.toml and opens that version's section;
the rest of the cycle adds to the section it opened. The guard asks for the bump
only while develop is still on the version main is on, because that version
is released and its notes describe something your change is not in.
The comparison is against main rather than against the tag, which matters in
the window where the release PR has merged but the tag is not pushed yet. What a
version contains is fixed when it reaches main, so a change landing after that
belongs to the next version, whatever the tags say.
A PR into main always needs the bump. It carries no new entries, only the
version they were written under, and main exists to be tagged: leaving it on
main's own version makes it un-taggable, since that version is on PyPI and any
other tag matches no file.
Verify and build live in the reusable workflow
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.
The PyPI project needs a one-time trusted-publisher registration 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, plus a GitHub Release |
Merge to main |
nothing, CI only | none |
N is github.run_number of the reusable workflow. It is 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 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, which is also why a spec of
insightfactory-cli>=1.0.0 skips develop-channel builds and an in-process
consumer that wants them must ask for insightfactory-cli>=1.0.0.dev0.
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 and names 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}usesgithub.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. That is 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. A merge that released nothing then looked the same as 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
differ:
- Unknown-option wording is aligned to
Unknown option '--flag'. Other argparse messages, such as a missing option value, still differ from Node'sutil.parseArgs. - Expiry timestamps in human-readable output use the C-locale
%cformat rather than JavaScriptDate#toLocaleString(). --versionreads the installed package metadata from1.0.0onward rather than an npmpackage.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.
Metadata
Release files for insightfactory-cli 1.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| insightfactory_cli-1.1.2.tar.gz | 191.8 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| insightfactory_cli-1.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 263.4 kB
Release files / insightfactory_cli-1.1.2.tar.gz
| Download URL | insightfactory_cli-1.1.2.tar.gz |
|---|---|
| Size | 191.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
82e796b3e79bdd151fbb421a72ee2c659164eccf0de6edb9e701f136a4050b89
|
|
BLAKE2b-256 checksum How to use checksums |
b694dd7f08a0a3a9d63c79fc7987523ed1133d5eac3ac75f1c747751303a60df
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency logRelease files / insightfactory_cli-1.1.2-py3-none-any.whl
| Download URL | insightfactory_cli-1.1.2-py3-none-any.whl |
|---|---|
| Size | 71.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fe1e6b4d10549a7a44905cdefb654be8ba381202b4a73f599f75e9e27363a05a
|
|
BLAKE2b-256 checksum How to use checksums |
87162eb74ee17de2fa418edfaab7f139b74f7ffa037fd5bbb37c65f9ce16eeb6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency logSecurity for insightfactory-cli 1.1.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for insightfactory_cli-1.1.2.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.