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.
- monorepoly-0.2.0/LICENSE +22 -0
- monorepoly-0.2.0/PKG-INFO +230 -0
- monorepoly-0.2.0/README.md +203 -0
- monorepoly-0.2.0/monorepoly/__init__.py +1 -0
- monorepoly-0.2.0/monorepoly/__main__.py +161 -0
- monorepoly-0.2.0/monorepoly/access.py +100 -0
- monorepoly-0.2.0/monorepoly/chunk.py +136 -0
- monorepoly-0.2.0/monorepoly/chunk_config.py +466 -0
- monorepoly-0.2.0/monorepoly/config.py +54 -0
- monorepoly-0.2.0/monorepoly/embed.py +66 -0
- monorepoly-0.2.0/monorepoly/exceptions.py +29 -0
- monorepoly-0.2.0/monorepoly/git.py +394 -0
- monorepoly-0.2.0/monorepoly/gitlab.py +234 -0
- monorepoly-0.2.0/monorepoly/hosted.py +155 -0
- monorepoly-0.2.0/monorepoly/indexer.py +557 -0
- monorepoly-0.2.0/monorepoly/judge.py +157 -0
- monorepoly-0.2.0/monorepoly/licensing.py +201 -0
- monorepoly-0.2.0/monorepoly/mcp_server.py +165 -0
- monorepoly-0.2.0/monorepoly/models.py +329 -0
- monorepoly-0.2.0/monorepoly/search.py +284 -0
- monorepoly-0.2.0/monorepoly/skill.py +23 -0
- monorepoly-0.2.0/monorepoly/skills/search/SKILL.md +28 -0
- monorepoly-0.2.0/monorepoly/store.py +290 -0
- monorepoly-0.2.0/monorepoly/trusted_keys/kimvais.pub +1 -0
- monorepoly-0.2.0/monorepoly/wizard.py +251 -0
- monorepoly-0.2.0/pyproject.toml +74 -0
- monorepoly-0.2.0/pyproject.toml.orig +72 -0
monorepoly-0.2.0/LICENSE
ADDED
|
@@ -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()
|