Metadata-Version: 2.4
Name: monorepoly
Version: 0.2.0
Summary: Org-wide code and activity index for LLM agents
License-Expression: LicenseRef-Monorepoly
License-File: LICENSE
Requires-Dist: fire>=0.7.1
Requires-Dist: rich>=15.0.0
Requires-Dist: cryptography>=50.0.2 ; extra == 'indexer'
Requires-Dist: google-genai>=2.28.0 ; extra == 'indexer'
Requires-Dist: pymongo>=4.18.2 ; extra == 'indexer'
Requires-Dist: python-gitlab>=8.6.0 ; extra == 'indexer'
Requires-Dist: pyyaml>=6.0.3 ; extra == 'indexer'
Requires-Dist: requests>=2.34.2 ; extra == 'indexer'
Requires-Dist: tenacity>=9.1.4 ; extra == 'indexer'
Requires-Dist: typesafe-sdk>=0.7.2 ; extra == 'indexer'
Requires-Dist: google-genai>=2.28.0 ; extra == 'server'
Requires-Dist: mcp>=2.2.0 ; extra == 'server'
Requires-Dist: pymongo>=4.18.2 ; extra == 'server'
Requires-Dist: python-gitlab>=8.6.0 ; extra == 'server'
Requires-Dist: requests>=2.34.2 ; extra == 'server'
Requires-Dist: typesafe-sdk>=0.7.2 ; extra == 'server'
Requires-Python: >=3.14
Provides-Extra: indexer
Provides-Extra: server
Description-Content-Type: text/markdown

# monorepoly

Org-wide index of code and engineering activity for LLM agents.

## Install

- Users of a hosted server: `uv tool install monorepoly` (or `pip install monorepoly`), then
  `monorepoly setup --server_url <url> --client_id <id>`. Nothing else is installed.
- Running the server yourself: `monorepoly[server]`; running the indexer: `monorepoly[indexer]`.
  A command whose extra is missing says which one to install.
- Images: the Dockerfile's `server` target runs `serve_http`, its `indexer` target (the
  default) the indexing commands.

## Setup

1. `docker compose up -d` (Atlas Local, for tests and local runs)
2. Set `GITLAB_GROUP_ID` (a group path or numeric id), `GITLAB_TOKEN`, `GITLAB_URL`,
   `GOOGLE_CLOUD_PROJECT`, `MONGODB_DB`, `MONGODB_URI`, `TYPESAFE_API_KEY`
3. `gcloud auth application-default login`

## Use

- Index every project in the group: `uv run monorepoly index_org`
  (archived projects are skipped; add `--include_archived` to index them too)
- Index one project's code, commits and branches:
  `uv run monorepoly index_project group/project`
- Sync one project's issues, merge requests and discussions:
  `uv run monorepoly sync_activity group/project`
- `--debug` skips every database write, but still makes the paid Vertex AI and Jev calls.
- Set up Claude Code: `monorepoly setup` prompts for the settings, checks GitLab, MongoDB and
  Google credentials, installs the skill and registers the server (`--debug` changes nothing
  and prints the `claude mcp add` command with secrets masked)
- Or register by hand with your own GitLab token, passing every setting with `-e` (see
  Security model):

  ```
  claude mcp add monorepoly -e GITLAB_GROUP_ID=... -e GITLAB_TOKEN=... -e GITLAB_URL=... \
    -e GOOGLE_CLOUD_PROJECT=... -e MONGODB_DB=... -e MONGODB_URI=... -e TYPESAFE_API_KEY=... \
    -- uv run --directory <this repo> monorepoly serve
  ```

- Skill: `monorepoly install-skill` copies it to `~/.claude/skills/`; rerun after upgrading
- Connect to a hosted server (see Self-host on Google Cloud):
  `monorepoly setup --server_url <url> --client_id <GitLab OAuth application id>`; Claude Code
  opens GitLab in the browser on first use

## Index from CI

The image runs the CLI; its entrypoint is the `monorepoly` command.

```
docker build --target indexer -t registry.gitlab.com/<group>/monorepoly:latest .
docker push registry.gitlab.com/<group>/monorepoly:latest
```

The job authenticates to Vertex AI with Workload Identity Federation: GitLab issues the job an
OIDC token, and GCP exchanges it for a service account. No key is stored anywhere.

One-time GCP setup:

1. Create a workload identity pool and an OIDC provider for `https://gitlab.com`, with the
   attribute mapping `google.subject=assertion.sub,attribute.namespace_path=assertion.namespace_path`
   and the attribute condition `assertion.namespace_path.startsWith('<group>')`, so only your
   group's pipelines can use it.
2. Grant a service account `roles/aiplatform.user`, and grant the pool's principals
   `roles/iam.workloadIdentityUser` on that service account.
3. Generate the credential config (it holds no secret):

   ```
   gcloud iam workload-identity-pools create-cred-config \
     projects/<number>/locations/global/workloadIdentityPools/<pool>/providers/<provider> \
     --service-account=<sa>@<project>.iam.gserviceaccount.com \
     --credential-source-file=/tmp/gcp-id-token --output-file=gcp-credentials.json
   ```

   and store its content in the CI variable `GCP_CREDENTIAL_CONFIG`.

CI variables (masked): `GITLAB_GROUP_ID`, `GITLAB_TOKEN` (a group access token with
`read_api` and `read_repository`), `GITLAB_URL`, `GOOGLE_CLOUD_PROJECT`, `MONGODB_DB`,
`MONGODB_URI`, `TYPESAFE_API_KEY`.

```yaml
monorepoly:
  image:
    name: registry.gitlab.com/<group>/monorepoly:latest
    entrypoint: [""]
  id_tokens:
    GCP_ID_TOKEN:
      aud: https://iam.googleapis.com/projects/<number>/locations/global/workloadIdentityPools/<pool>/providers/<provider>
  variables:
    GOOGLE_APPLICATION_CREDENTIALS: /tmp/gcp-credentials.json
  script:
    - echo "$GCP_ID_TOKEN" > /tmp/gcp-id-token
    - echo "$GCP_CREDENTIAL_CONFIG" > "$GOOGLE_APPLICATION_CREDENTIALS"
    - monorepoly index_project "$CI_PROJECT_PATH"
    - monorepoly sync_activity "$CI_PROJECT_PATH"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
```

A CI runner keeps no clone cache, so every run clones the repository again; only commits new
since the last run reach Jev and Vertex AI.

## Self-host on Google Cloud

`deploy/` is a Pulumi program that runs the MCP server as a Cloud Run service and `index_org` as
a Cloud Run job, with every credential in Secret Manager. Engineers connect over HTTP and log in
with GitLab; their laptops hold no MongoDB, Vertex AI or TypeSafe credentials.

You need: a GCP project, `gcloud auth application-default login`, Pulumi and an Atlas cluster.
The images come from ghcr.io, built by GitHub Actions on every push to `master` and tagged with
the commit; a deploy runs the images of the checkout's `HEAD`, so push and wait for the
`images` workflow before `pulumi up`.

1. In GitLab, create an OAuth application (group or instance level): scope `read_api`,
   redirect URI `http://localhost:41593/callback`, **Confidential** unchecked (Claude Code uses
   PKCE, so there is no secret). Its application id is public; you hand it to users.
2. In Atlas, create two database users for the index database: read-only for the server,
   read-write for the indexer.
3. Create a group access token with `read_api` and `read_repository` for the indexer.
4. Configure and deploy, keeping the state in your own project:

   ```
   cd deploy
   pulumi login gs://<state bucket>
   pulumi stack init prod
   pulumi config set gcp:project <project>
   pulumi config set gcp:region <region>
   pulumi config set gitlabGroupId <group path or id>
   pulumi config set mongodbDb <database>
   pulumi config set --secret gitlabToken <group access token>
   pulumi config set --secret mongodbUriReadonly <read-only URI>
   pulumi config set --secret mongodbUriReadwrite <read-write URI>
   pulumi config set --secret typesafeApiKey <key>
   pulumi up
   ```

   Optional: `gitlabUrl` (default `https://gitlab.com`), `schedule` (default hourly,
   `0 * * * *`), `indexerTimeout` (default `3300s`; keep it below the schedule's interval, so runs
   never overlap), `indexerMemory` (default `4Gi`; every run clones all repositories into memory),
   `license` (see License), `egressSubnetwork` (sends all traffic through that subnet, e.g. one
   whose Cloud NAT has a static IP on the Atlas IP access list), `schedulerRegion` (default
   `gcp:region`; Cloud Scheduler is missing from some regions, e.g. `europe-north1`),
   `imageTag` (default the checkout's commit), `imageRepository` (default `kimvais/monorepoly`).
5. Run the first full index by hand; it can take hours:
   `gcloud run jobs execute monorepoly-index --region <region> --task-timeout=24h --wait`
6. Start the schedule: `pulumi config set scheduleEnabled true && pulumi up`
7. Give users the `setup_command` output; set `oauthClientId` to your OAuth application id to
   have it filled in.

Every user sees only what their own GitLab account can see, as with a local server.

## License

Free for public repositories and open-source projects; private repositories and commercial
use need a commercial license (terms: [LICENSE](LICENSE)).

Public projects are indexed without a license. Private and internal projects need a license
file covering their group; without one the indexer skips them and stores nothing about them,
not even their names.

Give the indexer the file's content in `MONOREPOLY_LICENSE`. On Google Cloud:
`pulumi config set --secret license -- "$(cat monorepoly.license)" && pulumi up`.

A license file is TOML followed by an SSH signature over exactly the bytes above it. To issue
one, write the payload, with `key` set to the fingerprint `ssh-keygen -l -f <key>.pub` prints:

```toml
licensee = "Acme Oy"
gitlab_url = "https://gitlab.com"
groups = ["acme"]
expires = 2027-10-16
key = "SHA256:L2Z9F/e49TkoYH8+dWjsfpjJvuyJgDnkFLOOVlJDtpQ"
```

then sign it with a key whose `.pub` file is in `monorepoly/trusted_keys/`, and append the
signature:

```
ssh-keygen -Y sign -f ~/.ssh/id_ed25519 -n monorepoly-license acme.toml
cat acme.toml acme.toml.sig > monorepoly.license
```

The file must keep LF line endings: any changed byte breaks the signature. `groups` are full
paths and include their subgroups; `expires` is the last valid day.

## Security model

- The server filters every query by what your GitLab token can see: projects, per-feature access
  (repository, merge requests, issues) and confidential items. This protects against accidental
  exposure, not against anyone holding `MONGODB_URI`: the database holds the whole group's index,
  and the URI reads all of it directly.
- Give the server a read-only Atlas database user; only the indexer needs write access.
- The hosted server is public at the network level on purpose: it checks every request's GitLab
  token with GitLab, and accepts any of the user's tokens with `read_api`, which already reads
  everything the index would show them.
- Distribute the server settings only to people entitled to read the whole group.
- Register the settings with `claude mcp add -e KEY=VALUE ...` rather than in your shell profile,
  so an agent's shell commands cannot read them.

## Test

`uv run pytest` (needs Atlas Local). Live checks: `uv run pytest -m live`.
