diff --git a/Dockerfile b/Dockerfile index 8e97ece..17b4146 100644 --- a/Dockerfile +++ b/Dockerfile @@ -82,7 +82,7 @@ RUN sed -i \ RUN CGO_ENABLED=1 GOMAXPROCS=$(nproc) go build -tags musl -o paperless-gpt . # Stage 3: Create a lightweight image with just the binary -FROM alpine:latest +FROM alpine:3.21.2 ENV GIN_MODE=release diff --git a/README.md b/README.md index bfd10a3..0112328 100644 --- a/README.md +++ b/README.md @@ -179,35 +179,35 @@ services: # **Note:** When using Ollama, ensure that the Ollama server is running and accessible from the paperless-gpt container. -| Variable | Description | Required | -| -------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -------- | -| `PAPERLESS_BASE_URL` | URL of your paperless-ngx instance (e.g. `http://paperless-ngx:8000`). | Yes | -| `PAPERLESS_API_TOKEN` | API token for paperless-ngx. Generate one in paperless-ngx admin. | Yes | -| `PAPERLESS_PUBLIC_URL` | Public URL for Paperless (if different from `PAPERLESS_BASE_URL`). | No | -| `MANUAL_TAG` | Tag for manual processing. Default: `paperless-gpt`. | No | -| `AUTO_TAG` | Tag for auto processing. Default: `paperless-gpt-auto`. | No | -| `LLM_PROVIDER` | AI backend (`openai` or `ollama`). | Yes | -| `LLM_MODEL` | AI model name, e.g. `gpt-4o`, `gpt-3.5-turbo`, `deepseek-r1:8b`. | Yes | -| `OPENAI_API_KEY` | OpenAI API key (required if using OpenAI). | Cond. | -| `OPENAI_BASE_URL` | OpenAI base URL (optional, if using a custom OpenAI compatible service like LiteLLM). | No | -| `LLM_LANGUAGE` | Likely language for documents (e.g. `English`). Default: `English`. | No | -| `OLLAMA_HOST` | Ollama server URL (e.g. `http://host.docker.internal:11434`). | No | -| `OCR_PROVIDER` | OCR provider to use (`llm` or `google_docai`). Default: `llm`. | No | -| `VISION_LLM_PROVIDER` | AI backend for LLM OCR (`openai` or `ollama`). Required if OCR_PROVIDER is `llm`. | Cond. | -| `VISION_LLM_MODEL` | Model name for LLM OCR (e.g. `minicpm-v`). Required if OCR_PROVIDER is `llm`. | Cond. | -| `GOOGLE_PROJECT_ID` | Google Cloud project ID. Required if OCR_PROVIDER is `google_docai`. | Cond. | -| `GOOGLE_LOCATION` | Google Cloud region (e.g. `us`, `eu`). Required if OCR_PROVIDER is `google_docai`. | Cond. | -| `GOOGLE_PROCESSOR_ID` | Document AI processor ID. Required if OCR_PROVIDER is `google_docai`. | Cond. | -| `GOOGLE_APPLICATION_CREDENTIALS` | Path to the mounted Google service account key. Required if OCR_PROVIDER is `google_docai`. | Cond. | -| `AUTO_OCR_TAG` | Tag for automatically processing docs with OCR. Default: `paperless-gpt-ocr-auto`. | No | -| `LOG_LEVEL` | Application log level (`info`, `debug`, `warn`, `error`). Default: `info`. | No | -| `LISTEN_INTERFACE` | Network interface to listen on. Default: `:8080`. | No | -| `AUTO_GENERATE_TITLE` | Generate titles automatically if `paperless-gpt-auto` is used. Default: `true`. | No | -| `AUTO_GENERATE_TAGS` | Generate tags automatically if `paperless-gpt-auto` is used. Default: `true`. | No | -| `AUTO_GENERATE_CORRESPONDENTS` | Generate correspondents automatically if `paperless-gpt-auto` is used. Default: `true`. | No | -| `OCR_LIMIT_PAGES` | Limit the number of pages for OCR. Set to `0` for no limit. Default: `5`. | No | -| `TOKEN_LIMIT` | Maximum tokens allowed for prompts/content. Set to `0` to disable limit. Useful for smaller LLMs. | No | -| `CORRESPONDENT_BLACK_LIST` | A comma-separated list of names to exclude from the correspondents suggestions. Example: `John Doe, Jane Smith`. | No | +| Variable | Description | Required | Default | +| -------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -------- | ---------------------- | +| `PAPERLESS_BASE_URL` | URL of your paperless-ngx instance (e.g. `http://paperless-ngx:8000`). | Yes | | +| `PAPERLESS_API_TOKEN` | API token for paperless-ngx. Generate one in paperless-ngx admin. | Yes | | +| `PAPERLESS_PUBLIC_URL` | Public URL for Paperless (if different from `PAPERLESS_BASE_URL`). | No | | +| `MANUAL_TAG` | Tag for manual processing. | No | paperless-gpt | +| `AUTO_TAG` | Tag for auto processing. | No | paperless-gpt-auto | +| `LLM_PROVIDER` | AI backend (`openai` or `ollama`). | Yes | | +| `LLM_MODEL` | AI model name, e.g. `gpt-4o`, `gpt-3.5-turbo`, `deepseek-r1:8b`. | Yes | | +| `OPENAI_API_KEY` | OpenAI API key (required if using OpenAI). | Cond. | | +| `OPENAI_BASE_URL` | OpenAI base URL (optional, if using a custom OpenAI compatible service like LiteLLM). | No | | +| `LLM_LANGUAGE` | Likely language for documents (e.g. `English`). | No | English | +| `OLLAMA_HOST` | Ollama server URL (e.g. `http://host.docker.internal:11434`). | No | | +| `OCR_PROVIDER` | OCR provider to use (`llm` or `google_docai`). | No | llm | +| `VISION_LLM_PROVIDER` | AI backend for LLM OCR (`openai` or `ollama`). Required if OCR_PROVIDER is `llm`. | Cond. | | +| `VISION_LLM_MODEL` | Model name for LLM OCR (e.g. `minicpm-v`). Required if OCR_PROVIDER is `llm`. | Cond. | | +| `GOOGLE_PROJECT_ID` | Google Cloud project ID. Required if OCR_PROVIDER is `google_docai`. | Cond. | | +| `GOOGLE_LOCATION` | Google Cloud region (e.g. `us`, `eu`). Required if OCR_PROVIDER is `google_docai`. | Cond. | | +| `GOOGLE_PROCESSOR_ID` | Document AI processor ID. Required if OCR_PROVIDER is `google_docai`. | Cond. | | +| `GOOGLE_APPLICATION_CREDENTIALS` | Path to the mounted Google service account key. Required if OCR_PROVIDER is `google_docai`. | Cond. | | +| `AUTO_OCR_TAG` | Tag for automatically processing docs with OCR. | No | paperless-gpt-ocr-auto | +| `LOG_LEVEL` | Application log level (`info`, `debug`, `warn`, `error`). | No | info | +| `LISTEN_INTERFACE` | Network interface to listen on. | No | 8080 | +| `AUTO_GENERATE_TITLE` | Generate titles automatically if `paperless-gpt-auto` is used. | No | true | +| `AUTO_GENERATE_TAGS` | Generate tags automatically if `paperless-gpt-auto` is used. | No | true | +| `AUTO_GENERATE_CORRESPONDENTS` | Generate correspondents automatically if `paperless-gpt-auto` is used. | No | true | +| `OCR_LIMIT_PAGES` | Limit the number of pages for OCR. Set to `0` for no limit. | No | 5 | +| `TOKEN_LIMIT` | Maximum tokens allowed for prompts/content. Set to `0` to disable limit. Useful for smaller LLMs. | No | | +| `CORRESPONDENT_BLACK_LIST` | A comma-separated list of names to exclude from the correspondents suggestions. Example: `John Doe, Jane Smith`. | No | | ### Custom Prompt Templates diff --git a/main.go b/main.go index 68aefea..eb43498 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,8 @@ import ( "path/filepath" "runtime" "strconv" - "strings" + "slices" + "strings" "sync" "text/template" "time" @@ -187,6 +188,21 @@ func main() { ocrProvider: ocrProvider, } + if app.isOcrEnabled() { + fmt.Printf("Using %s as manual OCR tag\n", manualOcrTag) + fmt.Printf("Using %s as auto OCR tag\n", autoOcrTag) + rawLimitOcrPages := os.Getenv("OCR_LIMIT_PAGES") + if rawLimitOcrPages == "" { + limitOcrPages = 5 + } else { + var err error + limitOcrPages, err = strconv.Atoi(rawLimitOcrPages) + if err != nil { + log.Fatalf("Invalid OCR_LIMIT_PAGES value: %v", err) + } + } + } + // Start background process for auto-tagging go func() { minBackoffDuration := 10 * time.Second @@ -197,7 +213,7 @@ func main() { for { processedCount, err := func() (int, error) { count := 0 - if isOcrEnabled() { + if app.isOcrEnabled() { ocrCount, err := app.processAutoOcrTagDocuments() if err != nil { return 0, fmt.Errorf("error in processAutoOcrTagDocuments: %w", err) @@ -256,7 +272,7 @@ func main() { // Endpoint to see if user enabled OCR api.GET("/experimental/ocr", func(c *gin.Context) { - enabled := isOcrEnabled() + enabled := app.isOcrEnabled() c.JSON(http.StatusOK, gin.H{"enabled": enabled}) }) @@ -366,8 +382,8 @@ func initLogger() { }) } -func isOcrEnabled() bool { - return visionLlmModel != "" && visionLlmProvider != "" +func (app *App) isOcrEnabled() bool { + return app.ocrProvider != nil } // validateOrDefaultEnvVars ensures all necessary environment variables are set @@ -385,16 +401,10 @@ func validateOrDefaultEnvVars() { if manualOcrTag == "" { manualOcrTag = "paperless-gpt-ocr" } - if isOcrEnabled() { - fmt.Printf("Using %s as manual OCR tag\n", manualOcrTag) - } if autoOcrTag == "" { autoOcrTag = "paperless-gpt-ocr-auto" } - if isOcrEnabled() { - fmt.Printf("Using %s as auto OCR tag\n", autoOcrTag) - } if paperlessBaseURL == "" { log.Fatal("Please set the PAPERLESS_BASE_URL environment variable.") @@ -420,19 +430,6 @@ func validateOrDefaultEnvVars() { log.Fatal("Please set the OPENAI_API_KEY environment variable for OpenAI provider.") } - if isOcrEnabled() { - rawLimitOcrPages := os.Getenv("OCR_LIMIT_PAGES") - if rawLimitOcrPages == "" { - limitOcrPages = 5 - } else { - var err error - limitOcrPages, err = strconv.Atoi(rawLimitOcrPages) - if err != nil { - log.Fatalf("Invalid OCR_LIMIT_PAGES value: %v", err) - } - } - } - // Initialize token limit from environment variable if limit := os.Getenv("TOKEN_LIMIT"); limit != "" { if parsed, err := strconv.Atoi(limit); err == nil { @@ -466,7 +463,14 @@ func (app *App) processAutoTagDocuments() (int, error) { log.Debugf("Found at least %d remaining documents with tag %s", len(documents), autoTag) + processedCount := 0 for _, document := range documents { + // Skip documents that have the autoOcrTag + if slices.Contains(document.Tags, autoOcrTag) { + log.Debugf("Skipping document %d as it has the OCR tag %s", document.ID, autoOcrTag) + continue + } + docLogger := documentLogger(document.ID) docLogger.Info("Processing document for auto-tagging") @@ -479,17 +483,18 @@ func (app *App) processAutoTagDocuments() (int, error) { suggestions, err := app.generateDocumentSuggestions(ctx, suggestionRequest, docLogger) if err != nil { - return 0, fmt.Errorf("error generating suggestions for document %d: %w", document.ID, err) + return processedCount, fmt.Errorf("error generating suggestions for document %d: %w", document.ID, err) } err = app.Client.UpdateDocuments(ctx, suggestions, app.Database, false) if err != nil { - return 0, fmt.Errorf("error updating document %d: %w", document.ID, err) + return processedCount, fmt.Errorf("error updating document %d: %w", document.ID, err) } docLogger.Info("Successfully processed document") + processedCount++ } - return len(documents), nil + return processedCount, nil } // processAutoOcrTagDocuments handles the background auto-tagging of OCR documents diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..b3fd0ad --- /dev/null +++ b/main_test.go @@ -0,0 +1,177 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "slices" + "testing" + "text/template" + + "github.com/Masterminds/sprig/v3" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProcessAutoTagDocuments(t *testing.T) { + // Initialize required global variables + autoTag = "paperless-gpt-auto" + autoOcrTag = "paperless-gpt-ocr-auto" + + // Initialize templates + var err error + titleTemplate, err = template.New("title").Funcs(sprig.FuncMap()).Parse("") + require.NoError(t, err) + tagTemplate, err = template.New("tag").Funcs(sprig.FuncMap()).Parse("") + require.NoError(t, err) + correspondentTemplate, err = template.New("correspondent").Funcs(sprig.FuncMap()).Parse("") + require.NoError(t, err) + + // Create test environment + env := newTestEnv(t) + defer env.teardown() + + // Set up test cases + testCases := []struct { + name string + documents []Document + expectedCount int + expectedError string + updateResponse int // HTTP status code for update response + }{ + { + name: "Skip document with autoOcrTag", + documents: []Document{ + { + ID: 1, + Title: "Doc with OCR tag", + Tags: []string{autoTag, autoOcrTag}, + }, + { + ID: 2, + Title: "Doc without OCR tag", + Tags: []string{autoTag}, + }, + { + ID: 3, + Title: "Doc with OCR tag", + Tags: []string{autoTag, autoOcrTag}, + }, + }, + expectedCount: 1, + updateResponse: http.StatusOK, + }, + { + name: "No documents to process", + documents: []Document{}, + expectedCount: 0, + updateResponse: http.StatusOK, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Mock the GetAllTags response + env.setMockResponse("/api/tags/", func(w http.ResponseWriter, r *http.Request) { + response := map[string]interface{}{ + "results": []map[string]interface{}{ + {"id": 1, "name": autoTag}, + {"id": 2, "name": autoOcrTag}, + {"id": 3, "name": "other-tag"}, + }, + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) + }) + + // Mock the GetDocumentsByTags response + env.setMockResponse("/api/documents/", func(w http.ResponseWriter, r *http.Request) { + response := GetDocumentsApiResponse{ + Results: make([]GetDocumentApiResponseResult, len(tc.documents)), + } + for i, doc := range tc.documents { + tagIds := make([]int, len(doc.Tags)) + for j, tagName := range doc.Tags { + switch tagName { + case autoTag: + tagIds[j] = 1 + case autoOcrTag: + tagIds[j] = 2 + default: + tagIds[j] = 3 + } + } + response.Results[i] = GetDocumentApiResponseResult{ + ID: doc.ID, + Title: doc.Title, + Tags: tagIds, + Content: "Test content", + } + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) + }) + + // Mock the correspondent creation endpoint + env.setMockResponse("/api/correspondents/", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + // Mock successful correspondent creation + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]interface{}{ + "id": 3, + "name": "test response", + }) + } else { + // Mock GET response for existing correspondents + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{ + "results": []map[string]interface{}{ + {"id": 1, "name": "Alpha"}, + {"id": 2, "name": "Beta"}, + }, + }) + } + }) + + // Create test app + app := &App{ + Client: env.client, + Database: env.db, + LLM: &mockLLM{}, // Use mock LLM from app_llm_test.go + } + + // Set auto-generate flags + autoGenerateTitle = "true" + autoGenerateTags = "true" + autoGenerateCorrespondents = "true" + + // Mock the document update responses + for _, doc := range tc.documents { + if !slices.Contains(doc.Tags, autoOcrTag) { + updatePath := fmt.Sprintf("/api/documents/%d/", doc.ID) + env.setMockResponse(updatePath, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tc.updateResponse) + json.NewEncoder(w).Encode(map[string]interface{}{ + "id": doc.ID, + "title": "Updated " + doc.Title, + "tags": []int{1, 3}, // Mock updated tag IDs + }) + }) + } + } + + // Run the test + count, err := app.processAutoTagDocuments() + + // Verify results + if tc.expectedError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedError) + } else { + require.NoError(t, err) + assert.Equal(t, tc.expectedCount, count) + } + }) + } +} diff --git a/web-app/package-lock.json b/web-app/package-lock.json index d3d2de5..5b4bdcc 100644 --- a/web-app/package-lock.json +++ b/web-app/package-lock.json @@ -1663,17 +1663,17 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz", - "integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz", + "integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/type-utils": "8.23.0", - "@typescript-eslint/utils": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/scope-manager": "8.24.0", + "@typescript-eslint/type-utils": "8.24.0", + "@typescript-eslint/utils": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1693,16 +1693,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz", - "integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz", + "integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/scope-manager": "8.24.0", + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/typescript-estree": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4" }, "engines": { @@ -1718,14 +1718,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz", - "integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz", + "integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0" + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1736,14 +1736,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz", - "integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz", + "integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/utils": "8.23.0", + "@typescript-eslint/typescript-estree": "8.24.0", + "@typescript-eslint/utils": "8.24.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -1760,9 +1760,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", - "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz", + "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==", "dev": true, "license": "MIT", "engines": { @@ -1774,14 +1774,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", - "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz", + "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1827,16 +1827,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz", - "integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz", + "integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0" + "@typescript-eslint/scope-manager": "8.24.0", + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/typescript-estree": "8.24.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1851,13 +1851,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", - "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz", + "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/types": "8.24.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2983,9 +2983,9 @@ } }, "node_modules/eslint": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.0.tgz", - "integrity": "sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA==", + "version": "9.20.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", + "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", "dev": true, "license": "MIT", "dependencies": { @@ -4942,9 +4942,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -5460,15 +5460,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.23.0.tgz", - "integrity": "sha512-/LBRo3HrXr5LxmrdYSOCvoAMm7p2jNizNfbIpCgvG4HMsnoprRUOce/+8VJ9BDYWW68rqIENE/haVLWPeFZBVQ==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.24.0.tgz", + "integrity": "sha512-/lmv4366en/qbB32Vz5+kCNZEMf6xYHwh1z48suBwZvAtnXKbP+YhGe8OLE2BqC67LMqKkCNLtjejdwsdW6uOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.23.0", - "@typescript-eslint/parser": "8.23.0", - "@typescript-eslint/utils": "8.23.0" + "@typescript-eslint/eslint-plugin": "8.24.0", + "@typescript-eslint/parser": "8.24.0", + "@typescript-eslint/utils": "8.24.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0"