monorepoly 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ monorepoly license
2
+
3
+ Copyright (c) 2026 Kimmo Parviainen-Jalanko. All rights reserved.
4
+
5
+ 1. Free use. You may install and run this software (the "Software") free of charge to index
6
+ and search repositories, and their issues, merge requests and discussions, that are
7
+ publicly accessible, such as open-source projects.
8
+
9
+ 2. Commercial use. Any other use requires a commercial license from the copyright holder, in
10
+ particular:
11
+ a. indexing or searching private or internal repositories, and
12
+ b. using the Software in, or to provide, a commercial product or service.
13
+ A commercial license is a license file issued by the copyright holder; the Software reads it
14
+ from MONOREPOLY_LICENSE. Ask for one through https://monorepoly.com.
15
+
16
+ 3. Restrictions. You may not remove, disable or circumvent the Software's license checks, and
17
+ you may not redistribute the Software, modified or not, except as part of using it under
18
+ section 1 or 2.
19
+
20
+ 4. No warranty. The Software is provided "as is", without warranty of any kind, express or
21
+ implied. In no event shall the copyright holder be liable for any claim, damages or other
22
+ liability arising from the Software or its use.
@@ -0,0 +1,230 @@
1
+ Metadata-Version: 2.4
2
+ Name: monorepoly
3
+ Version: 0.2.0
4
+ Summary: Org-wide code and activity index for LLM agents
5
+ License-Expression: LicenseRef-Monorepoly
6
+ License-File: LICENSE
7
+ Requires-Dist: fire>=0.7.1
8
+ Requires-Dist: rich>=15.0.0
9
+ Requires-Dist: cryptography>=50.0.2 ; extra == 'indexer'
10
+ Requires-Dist: google-genai>=2.28.0 ; extra == 'indexer'
11
+ Requires-Dist: pymongo>=4.18.2 ; extra == 'indexer'
12
+ Requires-Dist: python-gitlab>=8.6.0 ; extra == 'indexer'
13
+ Requires-Dist: pyyaml>=6.0.3 ; extra == 'indexer'
14
+ Requires-Dist: requests>=2.34.2 ; extra == 'indexer'
15
+ Requires-Dist: tenacity>=9.1.4 ; extra == 'indexer'
16
+ Requires-Dist: typesafe-sdk>=0.7.2 ; extra == 'indexer'
17
+ Requires-Dist: google-genai>=2.28.0 ; extra == 'server'
18
+ Requires-Dist: mcp>=2.2.0 ; extra == 'server'
19
+ Requires-Dist: pymongo>=4.18.2 ; extra == 'server'
20
+ Requires-Dist: python-gitlab>=8.6.0 ; extra == 'server'
21
+ Requires-Dist: requests>=2.34.2 ; extra == 'server'
22
+ Requires-Dist: typesafe-sdk>=0.7.2 ; extra == 'server'
23
+ Requires-Python: >=3.14
24
+ Provides-Extra: indexer
25
+ Provides-Extra: server
26
+ Description-Content-Type: text/markdown
27
+
28
+ # monorepoly
29
+
30
+ Org-wide index of code and engineering activity for LLM agents.
31
+
32
+ ## Install
33
+
34
+ - Users of a hosted server: `uv tool install monorepoly` (or `pip install monorepoly`), then
35
+ `monorepoly setup --server_url <url> --client_id <id>`. Nothing else is installed.
36
+ - Running the server yourself: `monorepoly[server]`; running the indexer: `monorepoly[indexer]`.
37
+ A command whose extra is missing says which one to install.
38
+ - Images: the Dockerfile's `server` target runs `serve_http`, its `indexer` target (the
39
+ default) the indexing commands.
40
+
41
+ ## Setup
42
+
43
+ 1. `docker compose up -d` (Atlas Local, for tests and local runs)
44
+ 2. Set `GITLAB_GROUP_ID` (a group path or numeric id), `GITLAB_TOKEN`, `GITLAB_URL`,
45
+ `GOOGLE_CLOUD_PROJECT`, `MONGODB_DB`, `MONGODB_URI`, `TYPESAFE_API_KEY`
46
+ 3. `gcloud auth application-default login`
47
+
48
+ ## Use
49
+
50
+ - Index every project in the group: `uv run monorepoly index_org`
51
+ (archived projects are skipped; add `--include_archived` to index them too)
52
+ - Index one project's code, commits and branches:
53
+ `uv run monorepoly index_project group/project`
54
+ - Sync one project's issues, merge requests and discussions:
55
+ `uv run monorepoly sync_activity group/project`
56
+ - `--debug` skips every database write, but still makes the paid Vertex AI and Jev calls.
57
+ - Set up Claude Code: `monorepoly setup` prompts for the settings, checks GitLab, MongoDB and
58
+ Google credentials, installs the skill and registers the server (`--debug` changes nothing
59
+ and prints the `claude mcp add` command with secrets masked)
60
+ - Or register by hand with your own GitLab token, passing every setting with `-e` (see
61
+ Security model):
62
+
63
+ ```
64
+ claude mcp add monorepoly -e GITLAB_GROUP_ID=... -e GITLAB_TOKEN=... -e GITLAB_URL=... \
65
+ -e GOOGLE_CLOUD_PROJECT=... -e MONGODB_DB=... -e MONGODB_URI=... -e TYPESAFE_API_KEY=... \
66
+ -- uv run --directory <this repo> monorepoly serve
67
+ ```
68
+
69
+ - Skill: `monorepoly install-skill` copies it to `~/.claude/skills/`; rerun after upgrading
70
+ - Connect to a hosted server (see Self-host on Google Cloud):
71
+ `monorepoly setup --server_url <url> --client_id <GitLab OAuth application id>`; Claude Code
72
+ opens GitLab in the browser on first use
73
+
74
+ ## Index from CI
75
+
76
+ The image runs the CLI; its entrypoint is the `monorepoly` command.
77
+
78
+ ```
79
+ docker build --target indexer -t registry.gitlab.com/<group>/monorepoly:latest .
80
+ docker push registry.gitlab.com/<group>/monorepoly:latest
81
+ ```
82
+
83
+ The job authenticates to Vertex AI with Workload Identity Federation: GitLab issues the job an
84
+ OIDC token, and GCP exchanges it for a service account. No key is stored anywhere.
85
+
86
+ One-time GCP setup:
87
+
88
+ 1. Create a workload identity pool and an OIDC provider for `https://gitlab.com`, with the
89
+ attribute mapping `google.subject=assertion.sub,attribute.namespace_path=assertion.namespace_path`
90
+ and the attribute condition `assertion.namespace_path.startsWith('<group>')`, so only your
91
+ group's pipelines can use it.
92
+ 2. Grant a service account `roles/aiplatform.user`, and grant the pool's principals
93
+ `roles/iam.workloadIdentityUser` on that service account.
94
+ 3. Generate the credential config (it holds no secret):
95
+
96
+ ```
97
+ gcloud iam workload-identity-pools create-cred-config \
98
+ projects/<number>/locations/global/workloadIdentityPools/<pool>/providers/<provider> \
99
+ --service-account=<sa>@<project>.iam.gserviceaccount.com \
100
+ --credential-source-file=/tmp/gcp-id-token --output-file=gcp-credentials.json
101
+ ```
102
+
103
+ and store its content in the CI variable `GCP_CREDENTIAL_CONFIG`.
104
+
105
+ CI variables (masked): `GITLAB_GROUP_ID`, `GITLAB_TOKEN` (a group access token with
106
+ `read_api` and `read_repository`), `GITLAB_URL`, `GOOGLE_CLOUD_PROJECT`, `MONGODB_DB`,
107
+ `MONGODB_URI`, `TYPESAFE_API_KEY`.
108
+
109
+ ```yaml
110
+ monorepoly:
111
+ image:
112
+ name: registry.gitlab.com/<group>/monorepoly:latest
113
+ entrypoint: [""]
114
+ id_tokens:
115
+ GCP_ID_TOKEN:
116
+ aud: https://iam.googleapis.com/projects/<number>/locations/global/workloadIdentityPools/<pool>/providers/<provider>
117
+ variables:
118
+ GOOGLE_APPLICATION_CREDENTIALS: /tmp/gcp-credentials.json
119
+ script:
120
+ - echo "$GCP_ID_TOKEN" > /tmp/gcp-id-token
121
+ - echo "$GCP_CREDENTIAL_CONFIG" > "$GOOGLE_APPLICATION_CREDENTIALS"
122
+ - monorepoly index_project "$CI_PROJECT_PATH"
123
+ - monorepoly sync_activity "$CI_PROJECT_PATH"
124
+ rules:
125
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
126
+ ```
127
+
128
+ A CI runner keeps no clone cache, so every run clones the repository again; only commits new
129
+ since the last run reach Jev and Vertex AI.
130
+
131
+ ## Self-host on Google Cloud
132
+
133
+ `deploy/` is a Pulumi program that runs the MCP server as a Cloud Run service and `index_org` as
134
+ a Cloud Run job, with every credential in Secret Manager. Engineers connect over HTTP and log in
135
+ with GitLab; their laptops hold no MongoDB, Vertex AI or TypeSafe credentials.
136
+
137
+ You need: a GCP project, `gcloud auth application-default login`, Pulumi and an Atlas cluster.
138
+ The images come from ghcr.io, built by GitHub Actions on every push to `master` and tagged with
139
+ the commit; a deploy runs the images of the checkout's `HEAD`, so push and wait for the
140
+ `images` workflow before `pulumi up`.
141
+
142
+ 1. In GitLab, create an OAuth application (group or instance level): scope `read_api`,
143
+ redirect URI `http://localhost:41593/callback`, **Confidential** unchecked (Claude Code uses
144
+ PKCE, so there is no secret). Its application id is public; you hand it to users.
145
+ 2. In Atlas, create two database users for the index database: read-only for the server,
146
+ read-write for the indexer.
147
+ 3. Create a group access token with `read_api` and `read_repository` for the indexer.
148
+ 4. Configure and deploy, keeping the state in your own project:
149
+
150
+ ```
151
+ cd deploy
152
+ pulumi login gs://<state bucket>
153
+ pulumi stack init prod
154
+ pulumi config set gcp:project <project>
155
+ pulumi config set gcp:region <region>
156
+ pulumi config set gitlabGroupId <group path or id>
157
+ pulumi config set mongodbDb <database>
158
+ pulumi config set --secret gitlabToken <group access token>
159
+ pulumi config set --secret mongodbUriReadonly <read-only URI>
160
+ pulumi config set --secret mongodbUriReadwrite <read-write URI>
161
+ pulumi config set --secret typesafeApiKey <key>
162
+ pulumi up
163
+ ```
164
+
165
+ Optional: `gitlabUrl` (default `https://gitlab.com`), `schedule` (default hourly,
166
+ `0 * * * *`), `indexerTimeout` (default `3300s`; keep it below the schedule's interval, so runs
167
+ never overlap), `indexerMemory` (default `4Gi`; every run clones all repositories into memory),
168
+ `license` (see License), `egressSubnetwork` (sends all traffic through that subnet, e.g. one
169
+ whose Cloud NAT has a static IP on the Atlas IP access list), `schedulerRegion` (default
170
+ `gcp:region`; Cloud Scheduler is missing from some regions, e.g. `europe-north1`),
171
+ `imageTag` (default the checkout's commit), `imageRepository` (default `kimvais/monorepoly`).
172
+ 5. Run the first full index by hand; it can take hours:
173
+ `gcloud run jobs execute monorepoly-index --region <region> --task-timeout=24h --wait`
174
+ 6. Start the schedule: `pulumi config set scheduleEnabled true && pulumi up`
175
+ 7. Give users the `setup_command` output; set `oauthClientId` to your OAuth application id to
176
+ have it filled in.
177
+
178
+ Every user sees only what their own GitLab account can see, as with a local server.
179
+
180
+ ## License
181
+
182
+ Free for public repositories and open-source projects; private repositories and commercial
183
+ use need a commercial license (terms: [LICENSE](LICENSE)).
184
+
185
+ Public projects are indexed without a license. Private and internal projects need a license
186
+ file covering their group; without one the indexer skips them and stores nothing about them,
187
+ not even their names.
188
+
189
+ Give the indexer the file's content in `MONOREPOLY_LICENSE`. On Google Cloud:
190
+ `pulumi config set --secret license -- "$(cat monorepoly.license)" && pulumi up`.
191
+
192
+ A license file is TOML followed by an SSH signature over exactly the bytes above it. To issue
193
+ one, write the payload, with `key` set to the fingerprint `ssh-keygen -l -f <key>.pub` prints:
194
+
195
+ ```toml
196
+ licensee = "Acme Oy"
197
+ gitlab_url = "https://gitlab.com"
198
+ groups = ["acme"]
199
+ expires = 2027-10-16
200
+ key = "SHA256:L2Z9F/e49TkoYH8+dWjsfpjJvuyJgDnkFLOOVlJDtpQ"
201
+ ```
202
+
203
+ then sign it with a key whose `.pub` file is in `monorepoly/trusted_keys/`, and append the
204
+ signature:
205
+
206
+ ```
207
+ ssh-keygen -Y sign -f ~/.ssh/id_ed25519 -n monorepoly-license acme.toml
208
+ cat acme.toml acme.toml.sig > monorepoly.license
209
+ ```
210
+
211
+ The file must keep LF line endings: any changed byte breaks the signature. `groups` are full
212
+ paths and include their subgroups; `expires` is the last valid day.
213
+
214
+ ## Security model
215
+
216
+ - The server filters every query by what your GitLab token can see: projects, per-feature access
217
+ (repository, merge requests, issues) and confidential items. This protects against accidental
218
+ exposure, not against anyone holding `MONGODB_URI`: the database holds the whole group's index,
219
+ and the URI reads all of it directly.
220
+ - Give the server a read-only Atlas database user; only the indexer needs write access.
221
+ - The hosted server is public at the network level on purpose: it checks every request's GitLab
222
+ token with GitLab, and accepts any of the user's tokens with `read_api`, which already reads
223
+ everything the index would show them.
224
+ - Distribute the server settings only to people entitled to read the whole group.
225
+ - Register the settings with `claude mcp add -e KEY=VALUE ...` rather than in your shell profile,
226
+ so an agent's shell commands cannot read them.
227
+
228
+ ## Test
229
+
230
+ `uv run pytest` (needs Atlas Local). Live checks: `uv run pytest -m live`.
@@ -0,0 +1,203 @@
1
+ # monorepoly
2
+
3
+ Org-wide index of code and engineering activity for LLM agents.
4
+
5
+ ## Install
6
+
7
+ - Users of a hosted server: `uv tool install monorepoly` (or `pip install monorepoly`), then
8
+ `monorepoly setup --server_url <url> --client_id <id>`. Nothing else is installed.
9
+ - Running the server yourself: `monorepoly[server]`; running the indexer: `monorepoly[indexer]`.
10
+ A command whose extra is missing says which one to install.
11
+ - Images: the Dockerfile's `server` target runs `serve_http`, its `indexer` target (the
12
+ default) the indexing commands.
13
+
14
+ ## Setup
15
+
16
+ 1. `docker compose up -d` (Atlas Local, for tests and local runs)
17
+ 2. Set `GITLAB_GROUP_ID` (a group path or numeric id), `GITLAB_TOKEN`, `GITLAB_URL`,
18
+ `GOOGLE_CLOUD_PROJECT`, `MONGODB_DB`, `MONGODB_URI`, `TYPESAFE_API_KEY`
19
+ 3. `gcloud auth application-default login`
20
+
21
+ ## Use
22
+
23
+ - Index every project in the group: `uv run monorepoly index_org`
24
+ (archived projects are skipped; add `--include_archived` to index them too)
25
+ - Index one project's code, commits and branches:
26
+ `uv run monorepoly index_project group/project`
27
+ - Sync one project's issues, merge requests and discussions:
28
+ `uv run monorepoly sync_activity group/project`
29
+ - `--debug` skips every database write, but still makes the paid Vertex AI and Jev calls.
30
+ - Set up Claude Code: `monorepoly setup` prompts for the settings, checks GitLab, MongoDB and
31
+ Google credentials, installs the skill and registers the server (`--debug` changes nothing
32
+ and prints the `claude mcp add` command with secrets masked)
33
+ - Or register by hand with your own GitLab token, passing every setting with `-e` (see
34
+ Security model):
35
+
36
+ ```
37
+ claude mcp add monorepoly -e GITLAB_GROUP_ID=... -e GITLAB_TOKEN=... -e GITLAB_URL=... \
38
+ -e GOOGLE_CLOUD_PROJECT=... -e MONGODB_DB=... -e MONGODB_URI=... -e TYPESAFE_API_KEY=... \
39
+ -- uv run --directory <this repo> monorepoly serve
40
+ ```
41
+
42
+ - Skill: `monorepoly install-skill` copies it to `~/.claude/skills/`; rerun after upgrading
43
+ - Connect to a hosted server (see Self-host on Google Cloud):
44
+ `monorepoly setup --server_url <url> --client_id <GitLab OAuth application id>`; Claude Code
45
+ opens GitLab in the browser on first use
46
+
47
+ ## Index from CI
48
+
49
+ The image runs the CLI; its entrypoint is the `monorepoly` command.
50
+
51
+ ```
52
+ docker build --target indexer -t registry.gitlab.com/<group>/monorepoly:latest .
53
+ docker push registry.gitlab.com/<group>/monorepoly:latest
54
+ ```
55
+
56
+ The job authenticates to Vertex AI with Workload Identity Federation: GitLab issues the job an
57
+ OIDC token, and GCP exchanges it for a service account. No key is stored anywhere.
58
+
59
+ One-time GCP setup:
60
+
61
+ 1. Create a workload identity pool and an OIDC provider for `https://gitlab.com`, with the
62
+ attribute mapping `google.subject=assertion.sub,attribute.namespace_path=assertion.namespace_path`
63
+ and the attribute condition `assertion.namespace_path.startsWith('<group>')`, so only your
64
+ group's pipelines can use it.
65
+ 2. Grant a service account `roles/aiplatform.user`, and grant the pool's principals
66
+ `roles/iam.workloadIdentityUser` on that service account.
67
+ 3. Generate the credential config (it holds no secret):
68
+
69
+ ```
70
+ gcloud iam workload-identity-pools create-cred-config \
71
+ projects/<number>/locations/global/workloadIdentityPools/<pool>/providers/<provider> \
72
+ --service-account=<sa>@<project>.iam.gserviceaccount.com \
73
+ --credential-source-file=/tmp/gcp-id-token --output-file=gcp-credentials.json
74
+ ```
75
+
76
+ and store its content in the CI variable `GCP_CREDENTIAL_CONFIG`.
77
+
78
+ CI variables (masked): `GITLAB_GROUP_ID`, `GITLAB_TOKEN` (a group access token with
79
+ `read_api` and `read_repository`), `GITLAB_URL`, `GOOGLE_CLOUD_PROJECT`, `MONGODB_DB`,
80
+ `MONGODB_URI`, `TYPESAFE_API_KEY`.
81
+
82
+ ```yaml
83
+ monorepoly:
84
+ image:
85
+ name: registry.gitlab.com/<group>/monorepoly:latest
86
+ entrypoint: [""]
87
+ id_tokens:
88
+ GCP_ID_TOKEN:
89
+ aud: https://iam.googleapis.com/projects/<number>/locations/global/workloadIdentityPools/<pool>/providers/<provider>
90
+ variables:
91
+ GOOGLE_APPLICATION_CREDENTIALS: /tmp/gcp-credentials.json
92
+ script:
93
+ - echo "$GCP_ID_TOKEN" > /tmp/gcp-id-token
94
+ - echo "$GCP_CREDENTIAL_CONFIG" > "$GOOGLE_APPLICATION_CREDENTIALS"
95
+ - monorepoly index_project "$CI_PROJECT_PATH"
96
+ - monorepoly sync_activity "$CI_PROJECT_PATH"
97
+ rules:
98
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
99
+ ```
100
+
101
+ A CI runner keeps no clone cache, so every run clones the repository again; only commits new
102
+ since the last run reach Jev and Vertex AI.
103
+
104
+ ## Self-host on Google Cloud
105
+
106
+ `deploy/` is a Pulumi program that runs the MCP server as a Cloud Run service and `index_org` as
107
+ a Cloud Run job, with every credential in Secret Manager. Engineers connect over HTTP and log in
108
+ with GitLab; their laptops hold no MongoDB, Vertex AI or TypeSafe credentials.
109
+
110
+ You need: a GCP project, `gcloud auth application-default login`, Pulumi and an Atlas cluster.
111
+ The images come from ghcr.io, built by GitHub Actions on every push to `master` and tagged with
112
+ the commit; a deploy runs the images of the checkout's `HEAD`, so push and wait for the
113
+ `images` workflow before `pulumi up`.
114
+
115
+ 1. In GitLab, create an OAuth application (group or instance level): scope `read_api`,
116
+ redirect URI `http://localhost:41593/callback`, **Confidential** unchecked (Claude Code uses
117
+ PKCE, so there is no secret). Its application id is public; you hand it to users.
118
+ 2. In Atlas, create two database users for the index database: read-only for the server,
119
+ read-write for the indexer.
120
+ 3. Create a group access token with `read_api` and `read_repository` for the indexer.
121
+ 4. Configure and deploy, keeping the state in your own project:
122
+
123
+ ```
124
+ cd deploy
125
+ pulumi login gs://<state bucket>
126
+ pulumi stack init prod
127
+ pulumi config set gcp:project <project>
128
+ pulumi config set gcp:region <region>
129
+ pulumi config set gitlabGroupId <group path or id>
130
+ pulumi config set mongodbDb <database>
131
+ pulumi config set --secret gitlabToken <group access token>
132
+ pulumi config set --secret mongodbUriReadonly <read-only URI>
133
+ pulumi config set --secret mongodbUriReadwrite <read-write URI>
134
+ pulumi config set --secret typesafeApiKey <key>
135
+ pulumi up
136
+ ```
137
+
138
+ Optional: `gitlabUrl` (default `https://gitlab.com`), `schedule` (default hourly,
139
+ `0 * * * *`), `indexerTimeout` (default `3300s`; keep it below the schedule's interval, so runs
140
+ never overlap), `indexerMemory` (default `4Gi`; every run clones all repositories into memory),
141
+ `license` (see License), `egressSubnetwork` (sends all traffic through that subnet, e.g. one
142
+ whose Cloud NAT has a static IP on the Atlas IP access list), `schedulerRegion` (default
143
+ `gcp:region`; Cloud Scheduler is missing from some regions, e.g. `europe-north1`),
144
+ `imageTag` (default the checkout's commit), `imageRepository` (default `kimvais/monorepoly`).
145
+ 5. Run the first full index by hand; it can take hours:
146
+ `gcloud run jobs execute monorepoly-index --region <region> --task-timeout=24h --wait`
147
+ 6. Start the schedule: `pulumi config set scheduleEnabled true && pulumi up`
148
+ 7. Give users the `setup_command` output; set `oauthClientId` to your OAuth application id to
149
+ have it filled in.
150
+
151
+ Every user sees only what their own GitLab account can see, as with a local server.
152
+
153
+ ## License
154
+
155
+ Free for public repositories and open-source projects; private repositories and commercial
156
+ use need a commercial license (terms: [LICENSE](LICENSE)).
157
+
158
+ Public projects are indexed without a license. Private and internal projects need a license
159
+ file covering their group; without one the indexer skips them and stores nothing about them,
160
+ not even their names.
161
+
162
+ Give the indexer the file's content in `MONOREPOLY_LICENSE`. On Google Cloud:
163
+ `pulumi config set --secret license -- "$(cat monorepoly.license)" && pulumi up`.
164
+
165
+ A license file is TOML followed by an SSH signature over exactly the bytes above it. To issue
166
+ one, write the payload, with `key` set to the fingerprint `ssh-keygen -l -f <key>.pub` prints:
167
+
168
+ ```toml
169
+ licensee = "Acme Oy"
170
+ gitlab_url = "https://gitlab.com"
171
+ groups = ["acme"]
172
+ expires = 2027-10-16
173
+ key = "SHA256:L2Z9F/e49TkoYH8+dWjsfpjJvuyJgDnkFLOOVlJDtpQ"
174
+ ```
175
+
176
+ then sign it with a key whose `.pub` file is in `monorepoly/trusted_keys/`, and append the
177
+ signature:
178
+
179
+ ```
180
+ ssh-keygen -Y sign -f ~/.ssh/id_ed25519 -n monorepoly-license acme.toml
181
+ cat acme.toml acme.toml.sig > monorepoly.license
182
+ ```
183
+
184
+ The file must keep LF line endings: any changed byte breaks the signature. `groups` are full
185
+ paths and include their subgroups; `expires` is the last valid day.
186
+
187
+ ## Security model
188
+
189
+ - The server filters every query by what your GitLab token can see: projects, per-feature access
190
+ (repository, merge requests, issues) and confidential items. This protects against accidental
191
+ exposure, not against anyone holding `MONGODB_URI`: the database holds the whole group's index,
192
+ and the URI reads all of it directly.
193
+ - Give the server a read-only Atlas database user; only the indexer needs write access.
194
+ - The hosted server is public at the network level on purpose: it checks every request's GitLab
195
+ token with GitLab, and accepts any of the user's tokens with `read_api`, which already reads
196
+ everything the index would show them.
197
+ - Distribute the server settings only to people entitled to read the whole group.
198
+ - Register the settings with `claude mcp add -e KEY=VALUE ...` rather than in your shell profile,
199
+ so an agent's shell commands cannot read them.
200
+
201
+ ## Test
202
+
203
+ `uv run pytest` (needs Atlas Local). Live checks: `uv run pytest -m live`.
@@ -0,0 +1 @@
1
+ """Org-wide code and activity index for LLM agents."""
@@ -0,0 +1,161 @@
1
+ """Command line entry point: `monorepoly <command> [--debug]`."""
2
+
3
+ import functools
4
+ import importlib
5
+ import logging
6
+ import os
7
+ import re
8
+ from pathlib import Path
9
+ from types import ModuleType
10
+ from typing import TYPE_CHECKING
11
+
12
+ from monorepoly import config, skill, wizard
13
+ from monorepoly.exceptions import MonorepolyConfigError
14
+
15
+ if TYPE_CHECKING:
16
+ from monorepoly.indexer import IndexContext
17
+
18
+ _LOGGER = logging.getLogger(__name__)
19
+
20
+ _CACHE_DIR = Path.home() / '.cache' / 'monorepoly'
21
+ _DEFAULT_PORT = '8080'
22
+ _BACKTICKED = re.compile(r'`([^`]*)`')
23
+ _FALSE_WORDS = frozenset({'0', 'false', 'no'})
24
+ _TRUE_WORDS = frozenset({'1', 'true', 'yes'})
25
+
26
+
27
+ class _BacktickMarkupFormatter(logging.Formatter):
28
+ """Renders backticked log values as highlighted rich markup in the terminal."""
29
+
30
+ def format(self, record: logging.LogRecord) -> str:
31
+ from rich.markup import escape
32
+
33
+ # Escape first, so values cannot inject markup; escaping never touches backticks.
34
+ return _BACKTICKED.sub(r'[bold cyan]\1[/]', escape(super().format(record)))
35
+
36
+
37
+ def _parse_flag(value: object) -> bool:
38
+ """Returns `value` as a bool, raising on anything but a bool or a known true/false word."""
39
+ # Fire turns `--flag=false` into the truthy string 'false'; only 'False' becomes a bool.
40
+ if isinstance(value, bool):
41
+ return value
42
+ word = value.lower() if isinstance(value, str) else None
43
+ if word in _TRUE_WORDS:
44
+ return True
45
+ if word in _FALSE_WORDS:
46
+ return False
47
+ raise MonorepolyConfigError(f"Expected a boolean flag value, got '{value}'!")
48
+
49
+
50
+ class _Commands:
51
+ """Indexes the GitLab group and serves the index to agents."""
52
+
53
+ def __init__(self, debug: bool = False) -> None:
54
+ self._debug = debug
55
+
56
+ @functools.cached_property
57
+ def _settings(self) -> config.Settings:
58
+ # Lazy, so `install_skill` works before the indexer is configured.
59
+ return config.load_settings()
60
+
61
+ def install_skill(self, skills_dir: str = str(skill.SKILLS_DIR)) -> None:
62
+ """Installs the bundled Claude Code skill, replacing an older copy."""
63
+ skill.install_skill(dry_run=self._debug, skills_dir=Path(skills_dir))
64
+
65
+ def setup(self, server_url: str = '', client_id: str = '') -> None:
66
+ """Registers the server in Claude Code: hosted with `--server_url`, else prompts locally."""
67
+ from rich.console import Console
68
+
69
+ if server_url:
70
+ wizard.run_hosted(
71
+ client_id=client_id, console=Console(), dry_run=self._debug, server_url=server_url
72
+ )
73
+ return
74
+ # A local setup checks MongoDB, GitLab and Google credentials, and registers `serve`.
75
+ _load('mcp_server', extra='server')
76
+ wizard.run(console=Console(), dry_run=self._debug)
77
+
78
+ def index_org(self, include_archived: bool | str = False) -> None:
79
+ """Indexes every project in GITLAB_GROUP_ID, archived ones only if asked."""
80
+ indexer = _load('indexer', extra='indexer')
81
+ indexer.index_org(
82
+ ctx=self._index_context(),
83
+ group_id=self._settings.gitlab_group_id,
84
+ include_archived=_parse_flag(include_archived),
85
+ )
86
+
87
+ def index_project(self, path: str) -> None:
88
+ """Indexes the code and history of one `group/project`."""
89
+ indexer = _load('indexer', extra='indexer')
90
+ ctx = self._index_context()
91
+ ctx.store.ensure_indexes()
92
+ indexer.index_project(ctx=ctx, project=ctx.gitlab.project(path))
93
+
94
+ def sync_activity(self, path: str) -> None:
95
+ """Syncs issues, merge requests and discussions of one `group/project`."""
96
+ indexer = _load('indexer', extra='indexer')
97
+ ctx = self._index_context()
98
+ ctx.store.ensure_indexes()
99
+ indexer.sync_activity(ctx=ctx, project=ctx.gitlab.project(path))
100
+
101
+ def serve(self) -> None:
102
+ """Runs the MCP server on stdio."""
103
+ _load('mcp_server', extra='server').serve(self._settings)
104
+
105
+ def serve_http(self) -> None:
106
+ """Runs the hosted MCP server on HTTP for GitLab OAuth users, on port PORT."""
107
+ # Cloud Run tells the container its port in PORT.
108
+ port = int(os.environ.get('PORT', _DEFAULT_PORT))
109
+ hosted = _load('hosted', extra='server')
110
+ hosted.serve_http(port=port, settings=config.load_server_settings())
111
+
112
+ def _index_context(self) -> IndexContext:
113
+ settings = self._settings
114
+ modules = ('embed', 'gitlab', 'indexer', 'judge', 'licensing', 'store')
115
+ embed, gitlab, indexer, judge, licensing, store = (
116
+ _load(name, extra='indexer') for name in modules
117
+ )
118
+ return indexer.IndexContext(
119
+ cache_dir=_CACHE_DIR,
120
+ embedder=embed.make_embedder(project=settings.google_cloud_project),
121
+ gitlab=gitlab.make_gitlab(token=settings.gitlab_token, url=settings.gitlab_url),
122
+ gitlab_token=settings.gitlab_token,
123
+ judge=judge.make_judge(),
124
+ license=licensing.from_environ(os.environ),
125
+ # `debug` is a dry run: everything is computed, nothing is written.
126
+ store=store.Store(
127
+ db=store.connect(db=settings.mongodb_db, uri=settings.mongodb_uri),
128
+ dry_run=self._debug,
129
+ ),
130
+ )
131
+
132
+
133
+ def _load(module: str, *, extra: str) -> ModuleType:
134
+ """Returns `monorepoly.<module>`, or says which extra installs what it is missing."""
135
+ try:
136
+ return importlib.import_module(f'monorepoly.{module}')
137
+ except ModuleNotFoundError as err:
138
+ if (err.name or '').startswith('monorepoly'):
139
+ raise
140
+ raise MonorepolyConfigError(
141
+ f'`{err.name}` is missing, install the `{extra}` extra:'
142
+ f" pip install 'monorepoly[{extra}]'"
143
+ ) from err
144
+
145
+
146
+ def main() -> None:
147
+ import fire
148
+ from rich.console import Console
149
+ from rich.logging import RichHandler
150
+
151
+ # stdout carries the MCP protocol in `serve`, so all logging goes to stderr.
152
+ handler = RichHandler(console=Console(stderr=True), markup=True)
153
+ handler.setFormatter(_BacktickMarkupFormatter())
154
+ logging.basicConfig(handlers=[handler], level=logging.WARNING)
155
+ # Only our own progress at INFO: the SDKs and their HTTP clients log every request there.
156
+ logging.getLogger('monorepoly').setLevel(logging.INFO)
157
+ fire.Fire(_Commands)
158
+
159
+
160
+ if __name__ == '__main__':
161
+ main()