Compare commits

..

5 Commits

Author SHA1 Message Date
4ae92b4f85 chore: migrate module path from git.t-juice.club to code.t-juice.club
Update Go module path and all import references for Gitea to Forgejo
host migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 19:48:25 +01:00
4276ffbda5 feat: add optional basic auth support for Loki client
Some Loki deployments (e.g., behind a reverse proxy or Grafana Cloud)
require HTTP Basic Authentication. This adds optional --loki-username
and --loki-password flags (and corresponding env vars) to the
lab-monitoring server, along with NixOS module options for secure
credential management via systemd LoadCredential.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 20:32:10 +01:00
aff058dcc0 chore: update flake.lock
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 22:22:08 +01:00
dcaeb1f517 chore: remove unused gotools and go-tools from devShell
Both are redundant: staticcheck is covered by golangci-lint, and
gotools (goimports, etc.) is covered by gopls and golangci-lint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 22:20:48 +01:00
fd40e73f1b feat: add package indexing to MCP index_revision tool
The options server's index_revision now also indexes packages when running
under nixpkgs-search, matching the CLI behavior. The packages server gets
its own index_revision tool for standalone package indexing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 22:12:08 +01:00
27 changed files with 492 additions and 95 deletions

View File

@@ -25,6 +25,7 @@ Query Prometheus metrics, Alertmanager alerts, and Loki logs. Unlike other serve
- 8 core tools: list/get alerts, search metrics, get metadata, PromQL query, list targets, list/create silences - 8 core tools: list/get alerts, search metrics, get metadata, PromQL query, list targets, list/create silences
- 3 optional Loki tools (when `LOKI_URL` is set): query_logs, list_labels, list_label_values - 3 optional Loki tools (when `LOKI_URL` is set): query_logs, list_labels, list_label_values
- Configurable Prometheus, Alertmanager, and Loki URLs via flags or environment variables - Configurable Prometheus, Alertmanager, and Loki URLs via flags or environment variables
- Optional basic auth for Loki (`LOKI_USERNAME`/`LOKI_PASSWORD`)
### Git Explorer (`git-explorer`) ### Git Explorer (`git-explorer`)
Read-only access to git repository information. Designed for deployment verification. Read-only access to git repository information. Designed for deployment verification.
@@ -45,7 +46,7 @@ The nixpkgs/options/hm servers share a database-backed architecture:
- **Build System**: Nix flakes - **Build System**: Nix flakes
- **Databases**: PostgreSQL and SQLite (both fully supported) - **Databases**: PostgreSQL and SQLite (both fully supported)
- **Protocol**: MCP (Model Context Protocol) - JSON-RPC over STDIO or HTTP/SSE - **Protocol**: MCP (Model Context Protocol) - JSON-RPC over STDIO or HTTP/SSE
- **Module Path**: `git.t-juice.club/torjus/labmcp` - **Module Path**: `code.t-juice.club/torjus/labmcp`
## Project Status ## Project Status
@@ -144,7 +145,7 @@ labmcp/
| `search_options` | Full-text search across option names and descriptions | | `search_options` | Full-text search across option names and descriptions |
| `get_option` | Get full details for a specific option with children | | `get_option` | Get full details for a specific option with children |
| `get_file` | Fetch source file contents from indexed repository | | `get_file` | Fetch source file contents from indexed repository |
| `index_revision` | Index a revision (by hash or channel name) | | `index_revision` | Index a revision (options, files, and packages for nixpkgs) |
| `list_revisions` | List all indexed revisions | | `list_revisions` | List all indexed revisions |
| `delete_revision` | Delete an indexed revision | | `delete_revision` | Delete an indexed revision |
@@ -155,6 +156,7 @@ labmcp/
| `search_packages` | Full-text search across package names and descriptions | | `search_packages` | Full-text search across package names and descriptions |
| `get_package` | Get full details for a specific package by attr path | | `get_package` | Get full details for a specific package by attr path |
| `get_file` | Fetch source file contents from nixpkgs | | `get_file` | Fetch source file contents from nixpkgs |
| `index_revision` | Index a revision to make its packages searchable |
| `list_revisions` | List all indexed revisions | | `list_revisions` | List all indexed revisions |
| `delete_revision` | Delete an indexed revision | | `delete_revision` | Delete an indexed revision |

View File

@@ -59,25 +59,25 @@ Search and query NixOS configuration options. **Note**: Prefer using `nixpkgs-se
```bash ```bash
# Build the packages # Build the packages
nix build git+https://git.t-juice.club/torjus/labmcp#nixpkgs-search nix build git+https://code.t-juice.club/torjus/labmcp#nixpkgs-search
nix build git+https://git.t-juice.club/torjus/labmcp#hm-options nix build git+https://code.t-juice.club/torjus/labmcp#hm-options
nix build git+https://git.t-juice.club/torjus/labmcp#lab-monitoring nix build git+https://code.t-juice.club/torjus/labmcp#lab-monitoring
nix build git+https://git.t-juice.club/torjus/labmcp#git-explorer nix build git+https://code.t-juice.club/torjus/labmcp#git-explorer
# Or run directly # Or run directly
nix run git+https://git.t-juice.club/torjus/labmcp#nixpkgs-search -- --help nix run git+https://code.t-juice.club/torjus/labmcp#nixpkgs-search -- --help
nix run git+https://git.t-juice.club/torjus/labmcp#hm-options -- --help nix run git+https://code.t-juice.club/torjus/labmcp#hm-options -- --help
nix run git+https://git.t-juice.club/torjus/labmcp#lab-monitoring -- --help nix run git+https://code.t-juice.club/torjus/labmcp#lab-monitoring -- --help
nix run git+https://git.t-juice.club/torjus/labmcp#git-explorer -- --help nix run git+https://code.t-juice.club/torjus/labmcp#git-explorer -- --help
``` ```
### From Source ### From Source
```bash ```bash
go install git.t-juice.club/torjus/labmcp/cmd/nixpkgs-search@latest go install code.t-juice.club/torjus/labmcp/cmd/nixpkgs-search@latest
go install git.t-juice.club/torjus/labmcp/cmd/hm-options@latest go install code.t-juice.club/torjus/labmcp/cmd/hm-options@latest
go install git.t-juice.club/torjus/labmcp/cmd/lab-monitoring@latest go install code.t-juice.club/torjus/labmcp/cmd/lab-monitoring@latest
go install git.t-juice.club/torjus/labmcp/cmd/git-explorer@latest go install code.t-juice.club/torjus/labmcp/cmd/git-explorer@latest
``` ```
## Usage ## Usage
@@ -116,7 +116,9 @@ Configure in your MCP client (e.g., Claude Desktop):
"env": { "env": {
"PROMETHEUS_URL": "http://prometheus.example.com:9090", "PROMETHEUS_URL": "http://prometheus.example.com:9090",
"ALERTMANAGER_URL": "http://alertmanager.example.com:9093", "ALERTMANAGER_URL": "http://alertmanager.example.com:9093",
"LOKI_URL": "http://loki.example.com:3100" "LOKI_URL": "http://loki.example.com:3100",
"LOKI_USERNAME": "optional-username",
"LOKI_PASSWORD": "optional-password"
} }
}, },
"git-explorer": { "git-explorer": {
@@ -137,28 +139,28 @@ Alternatively, if you have Nix installed, you can use the flake directly without
"mcpServers": { "mcpServers": {
"nixpkgs-options": { "nixpkgs-options": {
"command": "nix", "command": "nix",
"args": ["run", "git+https://git.t-juice.club/torjus/labmcp#nixpkgs-search", "--", "options", "serve"], "args": ["run", "git+https://code.t-juice.club/torjus/labmcp#nixpkgs-search", "--", "options", "serve"],
"env": { "env": {
"NIXPKGS_SEARCH_DATABASE": "sqlite:///path/to/nixpkgs-search.db" "NIXPKGS_SEARCH_DATABASE": "sqlite:///path/to/nixpkgs-search.db"
} }
}, },
"nixpkgs-packages": { "nixpkgs-packages": {
"command": "nix", "command": "nix",
"args": ["run", "git+https://git.t-juice.club/torjus/labmcp#nixpkgs-search", "--", "packages", "serve"], "args": ["run", "git+https://code.t-juice.club/torjus/labmcp#nixpkgs-search", "--", "packages", "serve"],
"env": { "env": {
"NIXPKGS_SEARCH_DATABASE": "sqlite:///path/to/nixpkgs-search.db" "NIXPKGS_SEARCH_DATABASE": "sqlite:///path/to/nixpkgs-search.db"
} }
}, },
"hm-options": { "hm-options": {
"command": "nix", "command": "nix",
"args": ["run", "git+https://git.t-juice.club/torjus/labmcp#hm-options", "--", "serve"], "args": ["run", "git+https://code.t-juice.club/torjus/labmcp#hm-options", "--", "serve"],
"env": { "env": {
"HM_OPTIONS_DATABASE": "sqlite:///path/to/hm-options.db" "HM_OPTIONS_DATABASE": "sqlite:///path/to/hm-options.db"
} }
}, },
"lab-monitoring": { "lab-monitoring": {
"command": "nix", "command": "nix",
"args": ["run", "git+https://git.t-juice.club/torjus/labmcp#lab-monitoring", "--", "serve"], "args": ["run", "git+https://code.t-juice.club/torjus/labmcp#lab-monitoring", "--", "serve"],
"env": { "env": {
"PROMETHEUS_URL": "http://prometheus.example.com:9090", "PROMETHEUS_URL": "http://prometheus.example.com:9090",
"ALERTMANAGER_URL": "http://alertmanager.example.com:9093", "ALERTMANAGER_URL": "http://alertmanager.example.com:9093",
@@ -167,7 +169,7 @@ Alternatively, if you have Nix installed, you can use the flake directly without
}, },
"git-explorer": { "git-explorer": {
"command": "nix", "command": "nix",
"args": ["run", "git+https://git.t-juice.club/torjus/labmcp#git-explorer", "--", "serve"], "args": ["run", "git+https://code.t-juice.club/torjus/labmcp#git-explorer", "--", "serve"],
"env": { "env": {
"GIT_REPO_PATH": "/path/to/your/repo" "GIT_REPO_PATH": "/path/to/your/repo"
} }
@@ -351,6 +353,8 @@ hm-options delete release-23.11
| `PROMETHEUS_URL` | Prometheus base URL for lab-monitoring | `http://localhost:9090` | | `PROMETHEUS_URL` | Prometheus base URL for lab-monitoring | `http://localhost:9090` |
| `ALERTMANAGER_URL` | Alertmanager base URL for lab-monitoring | `http://localhost:9093` | | `ALERTMANAGER_URL` | Alertmanager base URL for lab-monitoring | `http://localhost:9093` |
| `LOKI_URL` | Loki base URL for lab-monitoring (optional, enables log tools) | *(none)* | | `LOKI_URL` | Loki base URL for lab-monitoring (optional, enables log tools) | *(none)* |
| `LOKI_USERNAME` | Username for Loki basic auth (optional) | *(none)* |
| `LOKI_PASSWORD` | Password for Loki basic auth (optional) | *(none)* |
### Database Connection Strings ### Database Connection Strings
@@ -384,7 +388,7 @@ hm-options -d "sqlite://my.db" index hm-unstable
| `search_options` | Search for options by name or description | | `search_options` | Search for options by name or description |
| `get_option` | Get full details for a specific option | | `get_option` | Get full details for a specific option |
| `get_file` | Fetch source file contents from the repository | | `get_file` | Fetch source file contents from the repository |
| `index_revision` | Index a revision | | `index_revision` | Index a revision (options, files, and packages for nixpkgs) |
| `list_revisions` | List all indexed revisions | | `list_revisions` | List all indexed revisions |
| `delete_revision` | Delete an indexed revision | | `delete_revision` | Delete an indexed revision |
@@ -395,7 +399,7 @@ hm-options -d "sqlite://my.db" index hm-unstable
| `search_packages` | Search for packages by name or description | | `search_packages` | Search for packages by name or description |
| `get_package` | Get full details for a specific package | | `get_package` | Get full details for a specific package |
| `get_file` | Fetch source file contents from nixpkgs | | `get_file` | Fetch source file contents from nixpkgs |
| `index_revision` | Index a revision | | `index_revision` | Index a revision to make its packages searchable |
| `list_revisions` | List all indexed revisions | | `list_revisions` | List all indexed revisions |
| `delete_revision` | Delete an indexed revision | | `delete_revision` | Delete an indexed revision |
@@ -439,7 +443,7 @@ The `nixpkgs-search` module runs two separate MCP servers (options and packages)
```nix ```nix
{ {
inputs.labmcp.url = "git+https://git.t-juice.club/torjus/labmcp"; inputs.labmcp.url = "git+https://code.t-juice.club/torjus/labmcp";
outputs = { self, nixpkgs, labmcp }: { outputs = { self, nixpkgs, labmcp }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
@@ -476,7 +480,7 @@ The `nixpkgs-search` module runs two separate MCP servers (options and packages)
```nix ```nix
{ {
inputs.labmcp.url = "git+https://git.t-juice.club/torjus/labmcp"; inputs.labmcp.url = "git+https://code.t-juice.club/torjus/labmcp";
outputs = { self, nixpkgs, labmcp }: { outputs = { self, nixpkgs, labmcp }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
@@ -499,7 +503,7 @@ The `nixpkgs-search` module runs two separate MCP servers (options and packages)
```nix ```nix
{ {
inputs.labmcp.url = "git+https://git.t-juice.club/torjus/labmcp"; inputs.labmcp.url = "git+https://code.t-juice.club/torjus/labmcp";
outputs = { self, nixpkgs, labmcp }: { outputs = { self, nixpkgs, labmcp }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
@@ -524,7 +528,7 @@ The `nixpkgs-search` module runs two separate MCP servers (options and packages)
```nix ```nix
{ {
inputs.labmcp.url = "git+https://git.t-juice.club/torjus/labmcp"; inputs.labmcp.url = "git+https://code.t-juice.club/torjus/labmcp";
outputs = { self, nixpkgs, labmcp }: { outputs = { self, nixpkgs, labmcp }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
@@ -547,7 +551,7 @@ The `nixpkgs-search` module runs two separate MCP servers (options and packages)
```nix ```nix
{ {
inputs.labmcp.url = "git+https://git.t-juice.club/torjus/labmcp"; inputs.labmcp.url = "git+https://code.t-juice.club/torjus/labmcp";
outputs = { self, nixpkgs, labmcp }: { outputs = { self, nixpkgs, labmcp }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
@@ -605,6 +609,8 @@ Both `options.http` and `packages.http` also support:
| `prometheusUrl` | string | `"http://localhost:9090"` | Prometheus base URL | | `prometheusUrl` | string | `"http://localhost:9090"` | Prometheus base URL |
| `alertmanagerUrl` | string | `"http://localhost:9093"` | Alertmanager base URL | | `alertmanagerUrl` | string | `"http://localhost:9093"` | Alertmanager base URL |
| `lokiUrl` | nullOr string | `null` | Loki base URL (enables log query tools when set) | | `lokiUrl` | nullOr string | `null` | Loki base URL (enables log query tools when set) |
| `lokiUsername` | nullOr string | `null` | Username for Loki basic authentication |
| `lokiPasswordFile` | nullOr path | `null` | Path to file containing Loki password (uses systemd `LoadCredential`) |
| `enableSilences` | bool | `false` | Enable the create_silence tool (write operation) | | `enableSilences` | bool | `false` | Enable the create_silence tool (write operation) |
| `http.address` | string | `"127.0.0.1:8084"` | HTTP listen address | | `http.address` | string | `"127.0.0.1:8084"` | HTTP listen address |
| `http.endpoint` | string | `"/mcp"` | HTTP endpoint path | | `http.endpoint` | string | `"/mcp"` | HTTP endpoint path |

View File

@@ -11,8 +11,8 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"git.t-juice.club/torjus/labmcp/internal/gitexplorer" "code.t-juice.club/torjus/labmcp/internal/gitexplorer"
"git.t-juice.club/torjus/labmcp/internal/mcp" "code.t-juice.club/torjus/labmcp/internal/mcp"
) )
const version = "0.1.0" const version = "0.1.0"

View File

@@ -12,10 +12,10 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"git.t-juice.club/torjus/labmcp/internal/database" "code.t-juice.club/torjus/labmcp/internal/database"
"git.t-juice.club/torjus/labmcp/internal/homemanager" "code.t-juice.club/torjus/labmcp/internal/homemanager"
"git.t-juice.club/torjus/labmcp/internal/mcp" "code.t-juice.club/torjus/labmcp/internal/mcp"
"git.t-juice.club/torjus/labmcp/internal/options" "code.t-juice.club/torjus/labmcp/internal/options"
) )
const ( const (

View File

@@ -11,11 +11,11 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"git.t-juice.club/torjus/labmcp/internal/mcp" "code.t-juice.club/torjus/labmcp/internal/mcp"
"git.t-juice.club/torjus/labmcp/internal/monitoring" "code.t-juice.club/torjus/labmcp/internal/monitoring"
) )
const version = "0.3.0" const version = "0.3.1"
func main() { func main() {
app := &cli.App{ app := &cli.App{
@@ -40,6 +40,16 @@ func main() {
Usage: "Loki base URL (optional, enables log query tools)", Usage: "Loki base URL (optional, enables log query tools)",
EnvVars: []string{"LOKI_URL"}, EnvVars: []string{"LOKI_URL"},
}, },
&cli.StringFlag{
Name: "loki-username",
Usage: "Username for Loki basic auth",
EnvVars: []string{"LOKI_USERNAME"},
},
&cli.StringFlag{
Name: "loki-password",
Usage: "Password for Loki basic auth",
EnvVars: []string{"LOKI_PASSWORD"},
},
}, },
Commands: []*cli.Command{ Commands: []*cli.Command{
serveCommand(), serveCommand(),
@@ -189,7 +199,11 @@ func runServe(c *cli.Context) error {
var loki *monitoring.LokiClient var loki *monitoring.LokiClient
if lokiURL := c.String("loki-url"); lokiURL != "" { if lokiURL := c.String("loki-url"); lokiURL != "" {
loki = monitoring.NewLokiClient(lokiURL) loki = monitoring.NewLokiClient(monitoring.LokiClientOptions{
BaseURL: lokiURL,
Username: c.String("loki-username"),
Password: c.String("loki-password"),
})
} }
config.InstructionsFunc = func() string { config.InstructionsFunc = func() string {
@@ -432,7 +446,11 @@ func runLogs(c *cli.Context, logql string) error {
} }
ctx := context.Background() ctx := context.Background()
loki := monitoring.NewLokiClient(lokiURL) loki := monitoring.NewLokiClient(monitoring.LokiClientOptions{
BaseURL: lokiURL,
Username: c.String("loki-username"),
Password: c.String("loki-password"),
})
now := time.Now() now := time.Now()
start, err := parseCLITime(c.String("start"), now.Add(-time.Hour)) start, err := parseCLITime(c.String("start"), now.Add(-time.Hour))
@@ -487,7 +505,11 @@ func runLabels(c *cli.Context) error {
} }
ctx := context.Background() ctx := context.Background()
loki := monitoring.NewLokiClient(lokiURL) loki := monitoring.NewLokiClient(monitoring.LokiClientOptions{
BaseURL: lokiURL,
Username: c.String("loki-username"),
Password: c.String("loki-password"),
})
if label := c.String("values"); label != "" { if label := c.String("values"); label != "" {
values, err := loki.LabelValues(ctx, label) values, err := loki.LabelValues(ctx, label)

View File

@@ -12,9 +12,9 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"git.t-juice.club/torjus/labmcp/internal/database" "code.t-juice.club/torjus/labmcp/internal/database"
"git.t-juice.club/torjus/labmcp/internal/mcp" "code.t-juice.club/torjus/labmcp/internal/mcp"
"git.t-juice.club/torjus/labmcp/internal/nixos" "code.t-juice.club/torjus/labmcp/internal/nixos"
) )
const ( const (

View File

@@ -12,15 +12,15 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"git.t-juice.club/torjus/labmcp/internal/database" "code.t-juice.club/torjus/labmcp/internal/database"
"git.t-juice.club/torjus/labmcp/internal/mcp" "code.t-juice.club/torjus/labmcp/internal/mcp"
"git.t-juice.club/torjus/labmcp/internal/nixos" "code.t-juice.club/torjus/labmcp/internal/nixos"
"git.t-juice.club/torjus/labmcp/internal/packages" "code.t-juice.club/torjus/labmcp/internal/packages"
) )
const ( const (
defaultDatabase = "sqlite://nixpkgs-search.db" defaultDatabase = "sqlite://nixpkgs-search.db"
version = "0.3.0" version = "0.4.0"
) )
func main() { func main() {
@@ -310,7 +310,8 @@ func runOptionsServe(c *cli.Context) error {
server := mcp.NewServer(store, logger, config) server := mcp.NewServer(store, logger, config)
indexer := nixos.NewIndexer(store) indexer := nixos.NewIndexer(store)
server.RegisterHandlers(indexer) pkgIndexer := packages.NewIndexer(store)
server.RegisterHandlersWithPackages(indexer, pkgIndexer)
transport := c.String("transport") transport := c.String("transport")
switch transport { switch transport {

6
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1770197578, "lastModified": 1770841267,
"narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=", "narHash": "sha256-9xejG0KoqsoKEGp2kVbXRlEYtFFcDTHjidiuX8hGO44=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", "rev": "ec7c70d12ce2fc37cb92aff673dcdca89d187bae",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -59,8 +59,6 @@
buildInputs = with pkgs; [ buildInputs = with pkgs; [
go go
gopls gopls
gotools
go-tools
golangci-lint golangci-lint
govulncheck govulncheck
postgresql postgresql

4
go.mod
View File

@@ -1,8 +1,9 @@
module git.t-juice.club/torjus/labmcp module code.t-juice.club/torjus/labmcp
go 1.24 go 1.24
require ( require (
github.com/go-git/go-git/v5 v5.16.4
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/urfave/cli/v2 v2.27.5 github.com/urfave/cli/v2 v2.27.5
modernc.org/sqlite v1.34.4 modernc.org/sqlite v1.34.4
@@ -19,7 +20,6 @@ require (
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.16.4 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect

35
go.sum
View File

@@ -5,6 +5,10 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
@@ -12,19 +16,28 @@ github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -36,21 +49,30 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
@@ -61,6 +83,8 @@ github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQ
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@@ -70,6 +94,8 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBi
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -82,21 +108,26 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=

View File

@@ -4,7 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"git.t-juice.club/torjus/labmcp/internal/mcp" "code.t-juice.club/torjus/labmcp/internal/mcp"
) )
// RegisterHandlers registers all git-explorer tool handlers on the MCP server. // RegisterHandlers registers all git-explorer tool handlers on the MCP server.

View File

@@ -15,9 +15,9 @@ import (
"strings" "strings"
"time" "time"
"git.t-juice.club/torjus/labmcp/internal/database" "code.t-juice.club/torjus/labmcp/internal/database"
"git.t-juice.club/torjus/labmcp/internal/nixos" "code.t-juice.club/torjus/labmcp/internal/nixos"
"git.t-juice.club/torjus/labmcp/internal/options" "code.t-juice.club/torjus/labmcp/internal/options"
) )
// revisionPattern validates revision strings to prevent injection attacks. // revisionPattern validates revision strings to prevent injection attacks.

View File

@@ -6,7 +6,7 @@ import (
"testing" "testing"
"time" "time"
"git.t-juice.club/torjus/labmcp/internal/database" "code.t-juice.club/torjus/labmcp/internal/database"
) )
// TestHomeManagerRevision is a known release branch for testing. // TestHomeManagerRevision is a known release branch for testing.

View File

@@ -8,18 +8,35 @@ import (
"strings" "strings"
"time" "time"
"git.t-juice.club/torjus/labmcp/internal/database" "code.t-juice.club/torjus/labmcp/internal/database"
"git.t-juice.club/torjus/labmcp/internal/options" "code.t-juice.club/torjus/labmcp/internal/options"
"git.t-juice.club/torjus/labmcp/internal/packages" "code.t-juice.club/torjus/labmcp/internal/packages"
) )
// RegisterHandlers registers all tool handlers on the server for options mode. // RegisterHandlers registers all tool handlers on the server for options mode.
// Used by legacy nixos-options and hm-options servers (no package indexing).
func (s *Server) RegisterHandlers(indexer options.Indexer) { func (s *Server) RegisterHandlers(indexer options.Indexer) {
s.registerOptionsHandlers(indexer, nil)
}
// RegisterHandlersWithPackages registers all tool handlers for options mode
// with additional package indexing support. When pkgIndexer is non-nil,
// index_revision will also index packages, and list_revisions will show package counts.
func (s *Server) RegisterHandlersWithPackages(indexer options.Indexer, pkgIndexer *packages.Indexer) {
s.registerOptionsHandlers(indexer, pkgIndexer)
}
// registerOptionsHandlers is the shared implementation for RegisterHandlers and RegisterHandlersWithPackages.
func (s *Server) registerOptionsHandlers(indexer options.Indexer, pkgIndexer *packages.Indexer) {
s.tools["search_options"] = s.handleSearchOptions s.tools["search_options"] = s.handleSearchOptions
s.tools["get_option"] = s.handleGetOption s.tools["get_option"] = s.handleGetOption
s.tools["get_file"] = s.handleGetFile s.tools["get_file"] = s.handleGetFile
s.tools["index_revision"] = s.makeIndexHandler(indexer) s.tools["index_revision"] = s.makeIndexHandler(indexer, pkgIndexer)
s.tools["list_revisions"] = s.handleListRevisions if pkgIndexer != nil {
s.tools["list_revisions"] = s.handleListRevisionsWithPackages
} else {
s.tools["list_revisions"] = s.handleListRevisions
}
s.tools["delete_revision"] = s.handleDeleteRevision s.tools["delete_revision"] = s.handleDeleteRevision
} }
@@ -28,6 +45,7 @@ func (s *Server) RegisterPackageHandlers(pkgIndexer *packages.Indexer) {
s.tools["search_packages"] = s.handleSearchPackages s.tools["search_packages"] = s.handleSearchPackages
s.tools["get_package"] = s.handleGetPackage s.tools["get_package"] = s.handleGetPackage
s.tools["get_file"] = s.handleGetFile s.tools["get_file"] = s.handleGetFile
s.tools["index_revision"] = s.makePackageIndexHandler(pkgIndexer)
s.tools["list_revisions"] = s.handleListRevisionsWithPackages s.tools["list_revisions"] = s.handleListRevisionsWithPackages
s.tools["delete_revision"] = s.handleDeleteRevision s.tools["delete_revision"] = s.handleDeleteRevision
} }
@@ -246,7 +264,8 @@ func (s *Server) handleGetFile(ctx context.Context, args map[string]interface{})
} }
// makeIndexHandler creates the index_revision handler with the indexer. // makeIndexHandler creates the index_revision handler with the indexer.
func (s *Server) makeIndexHandler(indexer options.Indexer) ToolHandler { // If pkgIndexer is non-nil, it will also index packages after options and files.
func (s *Server) makeIndexHandler(indexer options.Indexer, pkgIndexer *packages.Indexer) ToolHandler {
return func(ctx context.Context, args map[string]interface{}) (CallToolResult, error) { return func(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
revision, _ := args["revision"].(string) revision, _ := args["revision"].(string)
if revision == "" { if revision == "" {
@@ -278,6 +297,17 @@ func (s *Server) makeIndexHandler(indexer options.Indexer) ToolHandler {
s.logger.Printf("Warning: file indexing failed: %v", err) s.logger.Printf("Warning: file indexing failed: %v", err)
} }
// Index packages if package indexer is available
var packageCount int
if pkgIndexer != nil {
pkgResult, pkgErr := pkgIndexer.IndexPackages(ctx, result.Revision.ID, result.Revision.GitHash)
if pkgErr != nil {
s.logger.Printf("Warning: package indexing failed: %v", pkgErr)
} else {
packageCount = pkgResult.PackageCount
}
}
var sb strings.Builder var sb strings.Builder
sb.WriteString(fmt.Sprintf("Indexed revision: %s\n", result.Revision.GitHash)) sb.WriteString(fmt.Sprintf("Indexed revision: %s\n", result.Revision.GitHash))
if result.Revision.ChannelName != "" { if result.Revision.ChannelName != "" {
@@ -285,6 +315,9 @@ func (s *Server) makeIndexHandler(indexer options.Indexer) ToolHandler {
} }
sb.WriteString(fmt.Sprintf("Options: %d\n", result.OptionCount)) sb.WriteString(fmt.Sprintf("Options: %d\n", result.OptionCount))
sb.WriteString(fmt.Sprintf("Files: %d\n", fileCount)) sb.WriteString(fmt.Sprintf("Files: %d\n", fileCount))
if packageCount > 0 {
sb.WriteString(fmt.Sprintf("Packages: %d\n", packageCount))
}
// Handle Duration which may be time.Duration or interface{} // Handle Duration which may be time.Duration or interface{}
if dur, ok := result.Duration.(time.Duration); ok { if dur, ok := result.Duration.(time.Duration); ok {
sb.WriteString(fmt.Sprintf("Duration: %s\n", dur.Round(time.Millisecond))) sb.WriteString(fmt.Sprintf("Duration: %s\n", dur.Round(time.Millisecond)))
@@ -296,6 +329,85 @@ func (s *Server) makeIndexHandler(indexer options.Indexer) ToolHandler {
} }
} }
// makePackageIndexHandler creates an index_revision handler for the packages-only server.
// It creates a revision record if needed, then indexes packages.
func (s *Server) makePackageIndexHandler(pkgIndexer *packages.Indexer) ToolHandler {
return func(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
revision, _ := args["revision"].(string)
if revision == "" {
return ErrorContent(fmt.Errorf("revision is required")), nil
}
if err := packages.ValidateRevision(revision); err != nil {
return ErrorContent(err), nil
}
// Resolve channel aliases to git ref
ref := pkgIndexer.ResolveRevision(revision)
// Check if revision already exists
rev, err := s.store.GetRevision(ctx, ref)
if err != nil {
return ErrorContent(fmt.Errorf("failed to check revision: %w", err)), nil
}
if rev == nil {
// Also try by channel name
rev, err = s.store.GetRevisionByChannel(ctx, revision)
if err != nil {
return ErrorContent(fmt.Errorf("failed to check revision: %w", err)), nil
}
}
if rev == nil {
// Create a new revision record
commitDate, _ := pkgIndexer.GetCommitDate(ctx, ref)
rev = &database.Revision{
GitHash: ref,
ChannelName: pkgIndexer.GetChannelName(revision),
CommitDate: commitDate,
}
if err := s.store.CreateRevision(ctx, rev); err != nil {
return ErrorContent(fmt.Errorf("failed to create revision: %w", err)), nil
}
}
// Check if packages are already indexed for this revision
if rev.PackageCount > 0 {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Revision already indexed: %s\n", rev.GitHash))
if rev.ChannelName != "" {
sb.WriteString(fmt.Sprintf("Channel: %s\n", rev.ChannelName))
}
sb.WriteString(fmt.Sprintf("Packages: %d\n", rev.PackageCount))
sb.WriteString(fmt.Sprintf("Indexed at: %s\n", rev.IndexedAt.Format("2006-01-02 15:04")))
return CallToolResult{
Content: []Content{TextContent(sb.String())},
}, nil
}
// Index packages
pkgResult, err := pkgIndexer.IndexPackages(ctx, rev.ID, rev.GitHash)
if err != nil {
return ErrorContent(fmt.Errorf("package indexing failed: %w", err)), nil
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Indexed revision: %s\n", rev.GitHash))
if rev.ChannelName != "" {
sb.WriteString(fmt.Sprintf("Channel: %s\n", rev.ChannelName))
}
sb.WriteString(fmt.Sprintf("Packages: %d\n", pkgResult.PackageCount))
if dur, ok := pkgResult.Duration.(time.Duration); ok {
sb.WriteString(fmt.Sprintf("Duration: %s\n", dur.Round(time.Millisecond)))
}
return CallToolResult{
Content: []Content{TextContent(sb.String())},
}, nil
}
}
// handleListRevisions handles the list_revisions tool. // handleListRevisions handles the list_revisions tool.
func (s *Server) handleListRevisions(ctx context.Context, args map[string]interface{}) (CallToolResult, error) { func (s *Server) handleListRevisions(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
revisions, err := s.store.ListRevisions(ctx) revisions, err := s.store.ListRevisions(ctx)

View File

@@ -7,7 +7,7 @@ import (
"io" "io"
"log" "log"
"git.t-juice.club/torjus/labmcp/internal/database" "code.t-juice.club/torjus/labmcp/internal/database"
) )
// ServerMode indicates which type of tools the server should expose. // ServerMode indicates which type of tools the server should expose.
@@ -45,7 +45,7 @@ type ServerConfig struct {
func DefaultNixOSConfig() ServerConfig { func DefaultNixOSConfig() ServerConfig {
return ServerConfig{ return ServerConfig{
Name: "nixos-options", Name: "nixos-options",
Version: "0.3.0", Version: "0.4.0",
DefaultChannel: "nixos-stable", DefaultChannel: "nixos-stable",
SourceName: "nixpkgs", SourceName: "nixpkgs",
Mode: ModeOptions, Mode: ModeOptions,
@@ -57,7 +57,9 @@ If the current project contains a flake.lock file, you can index the exact nixpk
Example: If flake.lock contains "rev": "abc123...", call index_revision with revision "abc123...". Example: If flake.lock contains "rev": "abc123...", call index_revision with revision "abc123...".
This ensures option documentation matches the nixpkgs version the project actually uses.`, This ensures option documentation matches the nixpkgs version the project actually uses.
Note: index_revision also indexes packages when available, so both options and packages become searchable.`,
} }
} }
@@ -65,7 +67,7 @@ This ensures option documentation matches the nixpkgs version the project actual
func DefaultNixpkgsPackagesConfig() ServerConfig { func DefaultNixpkgsPackagesConfig() ServerConfig {
return ServerConfig{ return ServerConfig{
Name: "nixpkgs-packages", Name: "nixpkgs-packages",
Version: "0.3.0", Version: "0.4.0",
DefaultChannel: "nixos-stable", DefaultChannel: "nixos-stable",
SourceName: "nixpkgs", SourceName: "nixpkgs",
Mode: ModePackages, Mode: ModePackages,
@@ -73,7 +75,9 @@ func DefaultNixpkgsPackagesConfig() ServerConfig {
If the current project contains a flake.lock file, you can search packages from the exact nixpkgs revision used by the project: If the current project contains a flake.lock file, you can search packages from the exact nixpkgs revision used by the project:
1. Read the flake.lock file to find the nixpkgs "rev" field 1. Read the flake.lock file to find the nixpkgs "rev" field
2. Ensure the revision is indexed (packages are indexed separately from options) 2. Call index_revision with that git hash to index packages for that specific version
Example: If flake.lock contains "rev": "abc123...", call index_revision with revision "abc123...".
This ensures package information matches the nixpkgs version the project actually uses.`, This ensures package information matches the nixpkgs version the project actually uses.`,
} }
@@ -83,7 +87,7 @@ This ensures package information matches the nixpkgs version the project actuall
func DefaultMonitoringConfig() ServerConfig { func DefaultMonitoringConfig() ServerConfig {
return ServerConfig{ return ServerConfig{
Name: "lab-monitoring", Name: "lab-monitoring",
Version: "0.3.0", Version: "0.3.1",
Mode: ModeCustom, Mode: ModeCustom,
Instructions: `Lab Monitoring MCP Server - Query Prometheus metrics and Alertmanager alerts. Instructions: `Lab Monitoring MCP Server - Query Prometheus metrics and Alertmanager alerts.
@@ -427,7 +431,7 @@ func (s *Server) getOptionToolDefinitions() []Tool {
}, },
{ {
Name: "index_revision", Name: "index_revision",
Description: fmt.Sprintf("Index a %s revision to make its options searchable", sourceRepo), Description: s.indexRevisionDescription(sourceRepo),
InputSchema: InputSchema{ InputSchema: InputSchema{
Type: "object", Type: "object",
Properties: map[string]Property{ Properties: map[string]Property{
@@ -464,6 +468,15 @@ func (s *Server) getOptionToolDefinitions() []Tool {
} }
} }
// indexRevisionDescription returns the description for the index_revision tool,
// adjusted based on whether packages are also indexed.
func (s *Server) indexRevisionDescription(sourceRepo string) string {
if s.config.SourceName == "nixpkgs" {
return fmt.Sprintf("Index a %s revision to make its options and packages searchable", sourceRepo)
}
return fmt.Sprintf("Index a %s revision to make its options searchable", sourceRepo)
}
// getPackageToolDefinitions returns the tool definitions for packages mode. // getPackageToolDefinitions returns the tool definitions for packages mode.
func (s *Server) getPackageToolDefinitions() []Tool { func (s *Server) getPackageToolDefinitions() []Tool {
exampleChannels := "'nixos-unstable', 'nixos-24.05'" exampleChannels := "'nixos-unstable', 'nixos-24.05'"
@@ -547,6 +560,20 @@ func (s *Server) getPackageToolDefinitions() []Tool {
Required: []string{"path"}, Required: []string{"path"},
}, },
}, },
{
Name: "index_revision",
Description: "Index a nixpkgs revision to make its packages searchable",
InputSchema: InputSchema{
Type: "object",
Properties: map[string]Property{
"revision": {
Type: "string",
Description: fmt.Sprintf("Git hash (full or short) or channel name (e.g., %s)", exampleChannels),
},
},
Required: []string{"revision"},
},
},
{ {
Name: "list_revisions", Name: "list_revisions",
Description: "List all indexed nixpkgs revisions", Description: "List all indexed nixpkgs revisions",

View File

@@ -8,8 +8,9 @@ import (
"strings" "strings"
"testing" "testing"
"git.t-juice.club/torjus/labmcp/internal/database" "code.t-juice.club/torjus/labmcp/internal/database"
"git.t-juice.club/torjus/labmcp/internal/nixos" "code.t-juice.club/torjus/labmcp/internal/nixos"
"code.t-juice.club/torjus/labmcp/internal/packages"
) )
func TestServerInitialize(t *testing.T) { func TestServerInitialize(t *testing.T) {
@@ -145,6 +146,110 @@ func TestServerNotification(t *testing.T) {
} }
} }
func TestPackagesServerToolsList(t *testing.T) {
store := setupTestStore(t)
server := NewServer(store, nil, DefaultNixpkgsPackagesConfig())
pkgIndexer := packages.NewIndexer(store)
server.RegisterPackageHandlers(pkgIndexer)
input := `{"jsonrpc":"2.0","id":1,"method":"tools/list"}`
resp := runRequest(t, server, input)
if resp.Error != nil {
t.Fatalf("Unexpected error: %v", resp.Error)
}
result, ok := resp.Result.(map[string]interface{})
if !ok {
t.Fatalf("Expected map result, got %T", resp.Result)
}
tools, ok := result["tools"].([]interface{})
if !ok {
t.Fatalf("Expected tools array, got %T", result["tools"])
}
// Should have 6 tools (search_packages, get_package, get_file, index_revision, list_revisions, delete_revision)
if len(tools) != 6 {
t.Errorf("Expected 6 tools, got %d", len(tools))
}
expectedTools := map[string]bool{
"search_packages": false,
"get_package": false,
"get_file": false,
"index_revision": false,
"list_revisions": false,
"delete_revision": false,
}
for _, tool := range tools {
toolMap := tool.(map[string]interface{})
name := toolMap["name"].(string)
if _, ok := expectedTools[name]; ok {
expectedTools[name] = true
}
}
for name, found := range expectedTools {
if !found {
t.Errorf("Tool %q not found in tools list", name)
}
}
}
func TestOptionsServerWithPackagesToolsList(t *testing.T) {
store := setupTestStore(t)
server := NewServer(store, nil, DefaultNixOSConfig())
indexer := nixos.NewIndexer(store)
pkgIndexer := packages.NewIndexer(store)
server.RegisterHandlersWithPackages(indexer, pkgIndexer)
input := `{"jsonrpc":"2.0","id":1,"method":"tools/list"}`
resp := runRequest(t, server, input)
if resp.Error != nil {
t.Fatalf("Unexpected error: %v", resp.Error)
}
result, ok := resp.Result.(map[string]interface{})
if !ok {
t.Fatalf("Expected map result, got %T", resp.Result)
}
tools, ok := result["tools"].([]interface{})
if !ok {
t.Fatalf("Expected tools array, got %T", result["tools"])
}
// Should still have 6 tools (same as options-only)
if len(tools) != 6 {
t.Errorf("Expected 6 tools, got %d", len(tools))
}
// Verify index_revision is present
found := false
for _, tool := range tools {
toolMap := tool.(map[string]interface{})
if toolMap["name"].(string) == "index_revision" {
found = true
// For nixpkgs source, description should mention packages
desc := toolMap["description"].(string)
if !strings.Contains(desc, "packages") {
t.Errorf("index_revision description should mention packages, got: %s", desc)
}
break
}
}
if !found {
t.Error("index_revision tool not found in tools list")
}
}
func TestGetFilePathValidation(t *testing.T) { func TestGetFilePathValidation(t *testing.T) {
store := setupTestStore(t) store := setupTestStore(t)
server := setupTestServer(t, store) server := setupTestServer(t, store)

View File

@@ -7,7 +7,7 @@ import (
"strings" "strings"
"time" "time"
"git.t-juice.club/torjus/labmcp/internal/mcp" "code.t-juice.club/torjus/labmcp/internal/mcp"
) )
// AlertSummary queries Alertmanager for active (non-silenced) alerts and returns // AlertSummary queries Alertmanager for active (non-silenced) alerts and returns

View File

@@ -10,7 +10,7 @@ import (
"strings" "strings"
"testing" "testing"
"git.t-juice.club/torjus/labmcp/internal/mcp" "code.t-juice.club/torjus/labmcp/internal/mcp"
) )
// setupTestServer creates a test MCP server with monitoring handlers backed by test HTTP servers. // setupTestServer creates a test MCP server with monitoring handlers backed by test HTTP servers.
@@ -31,7 +31,7 @@ func setupTestServer(t *testing.T, promHandler, amHandler http.HandlerFunc, loki
var lokiSrv *httptest.Server var lokiSrv *httptest.Server
if len(lokiHandler) > 0 && lokiHandler[0] != nil { if len(lokiHandler) > 0 && lokiHandler[0] != nil {
lokiSrv = httptest.NewServer(lokiHandler[0]) lokiSrv = httptest.NewServer(lokiHandler[0])
loki = NewLokiClient(lokiSrv.URL) loki = NewLokiClient(LokiClientOptions{BaseURL: lokiSrv.URL})
} }
RegisterHandlers(server, prom, am, loki, HandlerOptions{EnableSilences: true}) RegisterHandlers(server, prom, am, loki, HandlerOptions{EnableSilences: true})

View File

@@ -11,16 +11,27 @@ import (
"time" "time"
) )
// LokiClientOptions configures the Loki client.
type LokiClientOptions struct {
BaseURL string
Username string
Password string
}
// LokiClient is an HTTP client for the Loki API. // LokiClient is an HTTP client for the Loki API.
type LokiClient struct { type LokiClient struct {
baseURL string baseURL string
username string
password string
httpClient *http.Client httpClient *http.Client
} }
// NewLokiClient creates a new Loki API client. // NewLokiClient creates a new Loki API client.
func NewLokiClient(baseURL string) *LokiClient { func NewLokiClient(opts LokiClientOptions) *LokiClient {
return &LokiClient{ return &LokiClient{
baseURL: strings.TrimRight(baseURL, "/"), baseURL: strings.TrimRight(opts.BaseURL, "/"),
username: opts.Username,
password: opts.Password,
httpClient: &http.Client{ httpClient: &http.Client{
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
}, },
@@ -94,6 +105,10 @@ func (c *LokiClient) get(ctx context.Context, path string, params url.Values) (j
return nil, fmt.Errorf("failed to create request: %w", err) return nil, fmt.Errorf("failed to create request: %w", err)
} }
if c.username != "" {
req.SetBasicAuth(c.username, c.password)
}
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("request failed: %w", err) return nil, fmt.Errorf("request failed: %w", err)

View File

@@ -42,7 +42,7 @@ func TestLokiClient_QueryRange(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
client := NewLokiClient(srv.URL) client := NewLokiClient(LokiClientOptions{BaseURL: srv.URL})
start := time.Unix(0, 1234567890000000000) start := time.Unix(0, 1234567890000000000)
end := time.Unix(0, 1234567899000000000) end := time.Unix(0, 1234567899000000000)
data, err := client.QueryRange(context.Background(), `{job="varlogs"}`, start, end, 10, "backward") data, err := client.QueryRange(context.Background(), `{job="varlogs"}`, start, end, 10, "backward")
@@ -78,7 +78,7 @@ func TestLokiClient_QueryRangeError(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
client := NewLokiClient(srv.URL) client := NewLokiClient(LokiClientOptions{BaseURL: srv.URL})
_, err := client.QueryRange(context.Background(), "invalid{", time.Now().Add(-time.Hour), time.Now(), 100, "backward") _, err := client.QueryRange(context.Background(), "invalid{", time.Now().Add(-time.Hour), time.Now(), 100, "backward")
if err == nil { if err == nil {
t.Fatal("expected error, got nil") t.Fatal("expected error, got nil")
@@ -102,7 +102,7 @@ func TestLokiClient_Labels(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
client := NewLokiClient(srv.URL) client := NewLokiClient(LokiClientOptions{BaseURL: srv.URL})
labels, err := client.Labels(context.Background()) labels, err := client.Labels(context.Background())
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
@@ -130,7 +130,7 @@ func TestLokiClient_LabelValues(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
client := NewLokiClient(srv.URL) client := NewLokiClient(LokiClientOptions{BaseURL: srv.URL})
values, err := client.LabelValues(context.Background(), "job") values, err := client.LabelValues(context.Background(), "job")
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
@@ -144,6 +144,65 @@ func TestLokiClient_LabelValues(t *testing.T) {
} }
} }
func TestLokiClient_BasicAuth(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()
if !ok {
t.Error("expected basic auth to be set")
}
if user != "myuser" {
t.Errorf("expected username=myuser, got %s", user)
}
if pass != "mypass" {
t.Errorf("expected password=mypass, got %s", pass)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"status": "success",
"data": ["job"]
}`))
}))
defer srv.Close()
client := NewLokiClient(LokiClientOptions{
BaseURL: srv.URL,
Username: "myuser",
Password: "mypass",
})
labels, err := client.Labels(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(labels) != 1 || labels[0] != "job" {
t.Errorf("unexpected labels: %v", labels)
}
}
func TestLokiClient_NoAuthWhenNoCredentials(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, _, ok := r.BasicAuth(); ok {
t.Error("expected no basic auth header, but it was set")
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"status": "success",
"data": ["job"]
}`))
}))
defer srv.Close()
client := NewLokiClient(LokiClientOptions{BaseURL: srv.URL})
labels, err := client.Labels(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(labels) != 1 || labels[0] != "job" {
t.Errorf("unexpected labels: %v", labels)
}
}
func TestLokiClient_HTTPError(t *testing.T) { func TestLokiClient_HTTPError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
@@ -151,7 +210,7 @@ func TestLokiClient_HTTPError(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
client := NewLokiClient(srv.URL) client := NewLokiClient(LokiClientOptions{BaseURL: srv.URL})
_, err := client.QueryRange(context.Background(), `{job="test"}`, time.Now().Add(-time.Hour), time.Now(), 100, "backward") _, err := client.QueryRange(context.Background(), `{job="test"}`, time.Now().Add(-time.Hour), time.Now(), 100, "backward")
if err == nil { if err == nil {
t.Fatal("expected error, got nil") t.Fatal("expected error, got nil")

View File

@@ -15,8 +15,8 @@ import (
"strings" "strings"
"time" "time"
"git.t-juice.club/torjus/labmcp/internal/database" "code.t-juice.club/torjus/labmcp/internal/database"
"git.t-juice.club/torjus/labmcp/internal/options" "code.t-juice.club/torjus/labmcp/internal/options"
) )
// revisionPattern validates revision strings to prevent injection attacks. // revisionPattern validates revision strings to prevent injection attacks.

View File

@@ -6,7 +6,7 @@ import (
"testing" "testing"
"time" "time"
"git.t-juice.club/torjus/labmcp/internal/database" "code.t-juice.club/torjus/labmcp/internal/database"
) )
// TestNixpkgsRevision is the revision from flake.lock used for testing. // TestNixpkgsRevision is the revision from flake.lock used for testing.

View File

@@ -4,7 +4,7 @@ package options
import ( import (
"context" "context"
"git.t-juice.club/torjus/labmcp/internal/database" "code.t-juice.club/torjus/labmcp/internal/database"
) )
// IndexResult contains the results of an indexing operation. // IndexResult contains the results of an indexing operation.

View File

@@ -12,7 +12,7 @@ import (
"strings" "strings"
"time" "time"
"git.t-juice.club/torjus/labmcp/internal/database" "code.t-juice.club/torjus/labmcp/internal/database"
) )
// revisionPattern validates revision strings to prevent injection attacks. // revisionPattern validates revision strings to prevent injection attacks.

View File

@@ -39,6 +39,18 @@ in
description = "Loki base URL. When set, enables log query tools (query_logs, list_labels, list_label_values)."; description = "Loki base URL. When set, enables log query tools (query_logs, list_labels, list_label_values).";
}; };
lokiUsername = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Username for Loki basic authentication.";
};
lokiPasswordFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Path to a file containing the password for Loki basic authentication. Recommended over storing secrets in the Nix store.";
};
enableSilences = lib.mkOption { enableSilences = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
@@ -112,12 +124,17 @@ in
ALERTMANAGER_URL = cfg.alertmanagerUrl; ALERTMANAGER_URL = cfg.alertmanagerUrl;
} // lib.optionalAttrs (cfg.lokiUrl != null) { } // lib.optionalAttrs (cfg.lokiUrl != null) {
LOKI_URL = cfg.lokiUrl; LOKI_URL = cfg.lokiUrl;
} // lib.optionalAttrs (cfg.lokiUsername != null) {
LOKI_USERNAME = cfg.lokiUsername;
}; };
script = let script = let
httpFlags = mkHttpFlags cfg.http; httpFlags = mkHttpFlags cfg.http;
silenceFlag = lib.optionalString cfg.enableSilences "--enable-silences"; silenceFlag = lib.optionalString cfg.enableSilences "--enable-silences";
in '' in ''
${lib.optionalString (cfg.lokiPasswordFile != null) ''
export LOKI_PASSWORD="$(< "$CREDENTIALS_DIRECTORY/loki-password")"
''}
exec ${cfg.package}/bin/lab-monitoring serve ${httpFlags} ${silenceFlag} exec ${cfg.package}/bin/lab-monitoring serve ${httpFlags} ${silenceFlag}
''; '';
@@ -126,7 +143,9 @@ in
DynamicUser = true; DynamicUser = true;
Restart = "on-failure"; Restart = "on-failure";
RestartSec = "5s"; RestartSec = "5s";
} // lib.optionalAttrs (cfg.lokiPasswordFile != null) {
LoadCredential = [ "loki-password:${cfg.lokiPasswordFile}" ];
} // {
# Hardening # Hardening
NoNewPrivileges = true; NoNewPrivileges = true;
ProtectSystem = "strict"; ProtectSystem = "strict";

View File

@@ -7,7 +7,7 @@
buildGoModule { buildGoModule {
inherit pname src; inherit pname src;
version = "0.3.0"; version = "0.4.0";
vendorHash = "sha256-XrTtiaQT5br+0ZXz8//rc04GZn/HlQk7l8Nx/+Uil/I="; vendorHash = "sha256-XrTtiaQT5br+0ZXz8//rc04GZn/HlQk7l8Nx/+Uil/I=";
@@ -22,7 +22,7 @@ buildGoModule {
meta = with lib; { meta = with lib; {
inherit description mainProgram; inherit description mainProgram;
homepage = "https://git.t-juice.club/torjus/labmcp"; homepage = "https://code.t-juice.club/torjus/labmcp";
license = licenses.mit; license = licenses.mit;
maintainers = [ ]; maintainers = [ ];
}; };