@ar-llm/pi-dir-providers
v0.1.5
Published
Pi extension that scopes the providers visible in /model based on the working directory.
Downloads
792
Maintainers
Readme
@ar-llm/pi-dir-providers
Pi extension that scopes which providers are visible in /model based on the
working directory. In ~/git you may only want anthropic and openrouter —
github-copilot and everything else disappear from model
selection entirely.
Install
pi install 'npm:@ar-llm/pi-dir-providers'Or try without installing:
pi -e 'npm:@ar-llm/pi-dir-providers'Config
Create <agentDir>/ar-llm/dir-providers.json (same convention as
pi-custom-compaction; <agentDir> is $PI_CODING_AGENT_DIR or
~/.config/pi/agent):
{
"rules": [
{
"dirs": ["~/git"],
"allowedProviders": ["anthropic", "openrouter"],
"defaultModel": "anthropic/claude-sonnet-4-5"
},
{
"dirs": ["~/git/acme-corp", "~/work/acme-corp"],
"allowedProviders": ["anthropic"],
"providers": {
"anthropic": { "baseUrl": "https://anthropic-proxy.acme.com/v1" }
}
}
]
}Rule ordering (important!)
Rules are applied strictly in array order — not by specificity. Every rule
whose dirs match the current working directory applies; for allowedProviders
and defaultModel, the last matching rule wins (its value replaces any
earlier rule's value). The providers field is different: overrides from all
matching rules are merged together per provider id.
You must order rules from least specific to most specific. Put the broadest directory rules first and the narrowest (most specific) directory rules last. If you reverse this order, a generic rule will silently override a specific one. For example, with rules
[{dirs: ["~/git/managing-construction"], allowedProviders: ["github-copilot"]}, {dirs: ["~/git"], allowedProviders: ["openrouter"]}], running in~/git/managing-construction/gossamermatches both rules, and the second rule'sopenrouterwins —github-copilotgets hidden. The correct order is to swap them so~/gitcomes first and~/git/managing-constructioncomes second.
dirs: list of directory subtrees. A leading~is expanded; a directory matches when the cwd equals it or is inside it (subdirectories inherit their ancestor's rules; a more-specific rule then overrides on top). Directory paths are symlink-resolved, so/tmpon macOS (which resolves to/private/tmp) matches the physical cwd. Nonexistent or inaccessible directories warn at startup and match as-is; point the rule at the real resolved path.allowedProviders: replaces the effective set of visible providers. Providers outside the set are hidden from/model(their models are removed for the session; auth//loginstate is untouched).defaultModel:"provider/model-id"applied to fresh sessions when the current model differs. Skipped when--model/--providerwas passed on the command line. Note that switching the model persists the choice tosettings.json, exactly like picking a model manually in/model.providers: per-provider overrides withmodels.jsonoverride semantics (e.g.baseUrl,headers), merged across matching rules per provider id.
If no rule matches, the extension does nothing.
How it works
Hiding happens at extension-factory time — before pi selects the initial
model
— by registering a models: [] overlay on each disallowed provider via
pi.registerProvider(). This goes through pi's normal provider-composition
path and is in-memory only: nothing on disk is touched, and /reload or a
new pi process restores the default provider set.
Provider overrides (baseUrl, headers, extra fields, models) are applied
the same way — as a registerProvider overlay — and are therefore also
in-memory and reversible. A models field in an override replaces the
provider's model set (like models.json), whereas an override object without
models only tweaks connection fields and keeps the provider's existing models.
Validation
- Missing config file, invalid JSON, or a missing
rulesarray disable the extension (it stays a no-op). - Per-rule problems (empty
dirs, relative dirs, duplicate dirs across rules, nonexistent dirs, malformeddefaultModel,defaultModelprovider not in the rule'sallowedProviders) produce warnings and skip the offending part. - Unknown provider ids in
allowedProviderswarn at startup; check actual ids withpi --list-models. - If a later rule's directory is a parent of an earlier rule's directory (i.e.
the broad rule appears after the narrow one), a warning is emitted at startup:
the parent rule's
allowedProviders/defaultModelwill silently override the child rule's, and reordering is needed to fix it.
Commands
/dir-providers— print the effective profile for the current directory: matched rule indices, allowed providers, default model, and merged provider overrides. The TUI status bar also shows the active provider set (dir-providers: providers: ...) whenever the extension is active.Config-loading warnings (nonexistent/inaccessible dirs, unknown providers, rule shadowing) are not printed to stderr by default — they would only appear before the TUI starts, where they're invisible. Instead they are captured and surfaced as a
Warning:notification in the TUI atsession_start, so they are always visible when running interactively.For headless or non-TUI runs (or to debug startup behavior), set
PI_DIR_PROVIDERS_DEBUG=1: diagnostics are written to$TMPDIR/ar-llm/dir-providers.log(never stderr).
Debug
The extension is quiet on stderr by default — diagnostics are routed to the TUI instead. To inspect raw startup behavior (e.g. for headless runs or to confirm which rules/providers resolved), enable verbose logging:
PI_DIR_PROVIDERS_DEBUG=1 pi ...Diagnostics are written to $TMPDIR/ar-llm/dir-providers.log, including:
- The
[dir-providers] Active in <cwd>: rules ..., allowed [...], hid N providersstatus line. - All captured config warnings (EPERM, unknown providers, rule shadowing).
Development
make typecheck # type-check source + tests
make test # run unit tests (26 checks across match.ts and config.ts)
make test-watch # same as `make test` but re-runs on file changesTests use Node.js's built-in node:test runtime — no external test runner
is required (Node ≥ 22.6). The test/ directory contains a small ESM loader
(loader.mjs + register.mjs) that maps NodeNext-style .js imports
to their .ts source files so they can be imported without a build step.
Limitations
- Providers registered by other extensions are not enumerated and cannot be hidden.
- Directory globs are not supported; use explicit directory subtrees.
- The profile is computed once per pi process from the startup cwd.
License
The Unlicense — public domain. Original work.
