From 7031052c4e7a675f03b653fa71e2f5a476233f3f Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Fri, 24 Apr 2026 15:56:12 +0200 Subject: [PATCH 1/7] :bug: Prevent invitations to blacklisted domains --- backend/scripts/_env | 2 +- .../app/rpc/commands/teams_invitations.clj | 7 ++++ backend/test/backend_tests/rpc_team_test.clj | 41 +++++++++++++++++++ frontend/src/app/main/ui/dashboard/team.cljs | 5 +++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/backend/scripts/_env b/backend/scripts/_env index e6ff68b7f4..5367a031b9 100644 --- a/backend/scripts/_env +++ b/backend/scripts/_env @@ -12,7 +12,7 @@ export PENPOT_PUBLIC_URI=https://localhost:3449 export PENPOT_FLAGS="\ $PENPOT_FLAGS \ - enable-login-with-password + enable-login-with-password \ disable-login-with-ldap \ disable-login-with-oidc \ disable-login-with-google \ diff --git a/backend/src/app/rpc/commands/teams_invitations.clj b/backend/src/app/rpc/commands/teams_invitations.clj index 5cffdd0c69..dfc83000a5 100644 --- a/backend/src/app/rpc/commands/teams_invitations.clj +++ b/backend/src/app/rpc/commands/teams_invitations.clj @@ -19,6 +19,7 @@ [app.config :as cf] [app.db :as db] [app.email :as eml] + [app.email.blacklist :as email.blacklist] [app.loggers.audit :as audit] [app.main :as-alias main] [app.rpc :as-alias rpc] @@ -91,6 +92,12 @@ (let [email (profile/clean-email email) member (profile/get-profile-by-email conn email)] + (when (and (email.blacklist/enabled? cfg) + (email.blacklist/contains? cfg email)) + (ex/raise :type :restriction + :code :email-domain-is-not-allowed + :hint "email domain is in the blacklist")) + ;; When we have email verification disabled and invitation user is ;; already present in the database, we proceed to add it to the ;; team as-is, without email roundtrip. diff --git a/backend/test/backend_tests/rpc_team_test.clj b/backend/test/backend_tests/rpc_team_test.clj index daf09e72a7..8fc553ff7e 100644 --- a/backend/test/backend_tests/rpc_team_test.clj +++ b/backend/test/backend_tests/rpc_team_test.clj @@ -11,6 +11,7 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] + [app.email.blacklist :as email.blacklist] [app.http :as http] [app.rpc :as-alias rpc] [app.storage :as sto] @@ -102,6 +103,46 @@ (t/is (= :validation (:type edata))) (t/is (= :member-is-muted (:code edata)))))))) +(t/deftest create-team-invitations-blacklisted-domain + (with-mocks [mock {:target 'app.email/send! :return nil}] + (let [profile1 (th/create-profile* 1 {:is-active true}) + team (th/create-team* 1 {:profile-id (:id profile1)}) + data {::th/type :create-team-invitations + ::rpc/profile-id (:id profile1) + :team-id (:id team) + :role :editor}] + + ;; invite from a directly blacklisted domain should fail + (with-redefs [email.blacklist/enabled? (constantly true) + email.blacklist/contains? (fn [_ email] + (clojure.string/ends-with? email "@blacklisted.com"))] + (let [out (th/command! (assoc data :emails ["user@blacklisted.com"]))] + (t/is (not (th/success? out))) + (t/is (= 0 (:call-count @mock))) + (let [edata (-> out :error ex-data)] + (t/is (= :restriction (:type edata))) + (t/is (= :email-domain-is-not-allowed (:code edata)))))) + + ;; invite from a subdomain of a blacklisted domain should also fail + (th/reset-mock! mock) + (with-redefs [email.blacklist/enabled? (constantly true) + email.blacklist/contains? (fn [_ email] + (clojure.string/ends-with? email "@sub.blacklisted.com"))] + (let [out (th/command! (assoc data :emails ["user@sub.blacklisted.com"]))] + (t/is (not (th/success? out))) + (t/is (= 0 (:call-count @mock))) + (let [edata (-> out :error ex-data)] + (t/is (= :restriction (:type edata))) + (t/is (= :email-domain-is-not-allowed (:code edata)))))) + + ;; invite from a non-blacklisted domain should succeed + (th/reset-mock! mock) + (with-redefs [email.blacklist/enabled? (constantly true) + email.blacklist/contains? (constantly false)] + (let [out (th/command! (assoc data :emails ["user@allowed.com"]))] + (t/is (th/success? out)) + (t/is (= 1 (:call-count @mock)))))))) + (t/deftest create-team-invitations-with-request-access (with-mocks [mock {:target 'app.email/send! :return nil}] (let [profile1 (th/create-profile* 1 {:is-active true}) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 96afda2563..993b1623a8 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -195,6 +195,11 @@ (= :email-has-complaints code)) (swap! error-text (tr "errors.email-spam-or-permanent-bounces" (:email error))) + (and (= :restriction type) + (= :email-domain-is-not-allowed code)) + (st/emit! (ntf/error (tr "errors.email-domain-not-allowed")) + (modal/hide)) + :else (st/emit! (ntf/error (tr "errors.generic")) (modal/hide))))) From 6d9019c3834c95176eb63dfb6cb3d9e329372cbf Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 25 Apr 2026 13:22:17 +0000 Subject: [PATCH 2/7] :books: Improve pull request documentation in CONTRIBUTING.md Expand the Pull Requests section with detailed guidance on PR title format, description expectations, branch naming conventions, the review process, and a list of PRs that will not be accepted. Also clarify the 'Discuss Before Building' rule to link to GitHub Issues and Discussions and reference Taiga stories. Update the Table of Contents with nested links for all new subsections. Signed-off-by: Andrey Antukh --- CONTRIBUTING.md | 101 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e8f30e605..d733ea5c7a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,17 @@ Center](https://help.penpot.app/). - [Prerequisites](#prerequisites) - [Reporting Bugs](#reporting-bugs) - [Pull Requests](#pull-requests) + - [Workflow](#workflow) + - [Title format](#title-format) + - [Description](#description) + - [Branch naming](#branch-naming) + - [Review process](#review-process) + - [What we won't accept](#what-we-wont-accept) + - [Good first issues](#good-first-issues) - [Commit Guidelines](#commit-guidelines) + - [Commit types](#commit-types) + - [Rules](#rules) + - [Examples](#examples) - [Formatting and Linting](#formatting-and-linting) - [Changelog](#changelog) - [Code of Conduct](#code-of-conduct) @@ -52,15 +62,98 @@ Advisories](https://github.com/penpot/penpot/security/advisories) 1. **Read the DCO** — see [Developer's Certificate of Origin](#developers-certificate-of-origin-dco) below. All code patches must include a `Signed-off-by` line. -2. **Discuss before building** — open a question/discussion issue before - starting work on a new feature or significant change. No PR will be - accepted without prior discussion, whether it is a new feature, a planned - one, or a quick win. +2. **Discuss before building** — open a [GitHub + Issue](https://github.com/penpot/penpot/issues) or start a [GitHub + Discussion](https://github.com/penpot/penpot/discussions) before starting + work on a new feature or significant change. For planned features on the + roadmap, reference the corresponding Taiga story. No PR will be accepted + without prior discussion, whether it is a new feature, a planned one, or a + quick win. 3. **Bug fixes** — you may submit a PR directly, but we still recommend filing an issue first so we can track it independently of your fix. 4. **Format and lint** — run the checks described in [Formatting and Linting](#formatting-and-linting) before submitting. +### Title format + +Pull request titles **must** follow the same convention as commit subjects: + +``` +:emoji: +``` + +- Use the **imperative mood** (e.g. "Fix", not "Fixed"). +- Capitalize the first letter of the subject. +- Do not end the subject with a period. +- Keep the subject to **70 characters** or fewer. +- Use one of the [commit type emojis](#commit-types) listed below. + +When a PR contains multiple unrelated commits, choose the emoji that +best represents the dominant change. + +**Examples:** + +``` +:bug: Fix unexpected error on launching modal +:sparkles: Enable new modal for profile +:zap: Improve performance of dashboard navigation +``` + +> **Note:** When a PR is squash-merged, the PR title becomes the +> commit message on the main branch. Getting the title right matters. + +### Description + +Every pull request should include a description that helps reviewers +understand the change quickly: + +1. **What and why** — describe the change and its motivation. +2. **Link related issues** — use `Closes #1234` or reference a Taiga + story (e.g. `Taiga #5678`). +3. **Screenshots or recordings** — required for any UI-visible change. +4. **Testing notes** — how did you verify the change? Any edge cases? +5. **Breaking changes** — call out anything that affects existing users + or requires migration steps. + +### Branch naming + +Use a descriptive branch name that reflects the type and scope of the +change: + +``` +/ +``` + +Types: `fix`, `feat`, `refactor`, `docs`, `chore`, `perf`. + +Optionally include the issue number: + +``` +fix/9122-email-blacklisting +feat/export-webp +refactor/layout-sizing +``` + +### Review process + +- Maintainers review PRs when time permits. Please be patient. +- Address review feedback by **pushing new commits** — do not + force-push during review, as it breaks comment threads. +- PRs require at least **one approval** before merge. +- We use **squash-merge** by default. The PR title becomes the final + commit message, so follow the [title format](#title-format) above. + +### What we won't accept + +To save time on both sides, please avoid submitting PRs that: + +- Introduce new dependencies without prior discussion. +- Change the build system or CI configuration without maintainer + approval. +- Mix unrelated changes in a single PR — keep PRs focused on one + concern. +- Skip the [discussion step](#workflow) for non-bug-fix changes. + ### Good first issues We use the `easy fix` label to mark issues appropriate for newcomers. From 37cba3355d8af186563ab584748fd375270e2cdb Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 25 Apr 2026 13:27:11 +0000 Subject: [PATCH 3/7] :wrench: Update opencode tooling, agents, and devenv Update agent configurations: change commiter mode to all, rename engineer agent to "Penpot Engineer", and remove obsolete testing agent. Add new read-only planner agent for architecture analysis and planning. Add four new skills: bat-cat (syntax-highlighted cat clone), fd-find (fast file finder), jq-json-processor (JSON processor), and ripgrep (fast text search). Add fd-find and bat packages to devenv Dockerfile. Update .gitignore to exclude opencode package-lock and plans directory. Signed-off-by: Andrey Antukh --- .gitignore | 3 + .opencode/agents/commiter.md | 8 +- .opencode/agents/engineer.md | 2 +- .opencode/agents/planner.md | 61 ++++++ .opencode/agents/prompt-assistant.md | 59 ++++++ .opencode/agents/testing.md | 37 ---- .opencode/skills/bat-cat/SKILL.md | 210 ++++++++++++++++++++ .opencode/skills/fd-find/SKILL.md | 194 ++++++++++++++++++ .opencode/skills/jq-json-processor/SKILL.md | 112 +++++++++++ .opencode/skills/ripgrep/SKILL.md | 150 ++++++++++++++ docker/devenv/Dockerfile | 3 + 11 files changed, 798 insertions(+), 41 deletions(-) create mode 100644 .opencode/agents/planner.md create mode 100644 .opencode/agents/prompt-assistant.md delete mode 100644 .opencode/agents/testing.md create mode 100644 .opencode/skills/bat-cat/SKILL.md create mode 100644 .opencode/skills/fd-find/SKILL.md create mode 100644 .opencode/skills/jq-json-processor/SKILL.md create mode 100644 .opencode/skills/ripgrep/SKILL.md diff --git a/.gitignore b/.gitignore index 5d718c3363..6a23f2101f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,9 @@ /.clj-kondo/.cache /_dump /notes +/.opencode/package-lock.json +/plans +/prompts /playground/ /backend/*.md !/backend/AGENTS.md diff --git a/.opencode/agents/commiter.md b/.opencode/agents/commiter.md index 3020e5aac1..a0ac40e443 100644 --- a/.opencode/agents/commiter.md +++ b/.opencode/agents/commiter.md @@ -1,11 +1,13 @@ --- name: commiter description: Git commit assistant following CONTRIBUTING.md commit rules -mode: primary +mode: subagent --- -Role: You are responsible for creating git commits for Penpot and must follow -the repository commit-format rules exactly. +Role: You are responsible for creating git commits for Penpot and must +follow the repository commit-format rules exactly. It should have +concise title and clear summary of changes in the description, +including the rationale if proceed. Requirements: diff --git a/.opencode/agents/engineer.md b/.opencode/agents/engineer.md index b85205f031..b5ba1bf3f7 100644 --- a/.opencode/agents/engineer.md +++ b/.opencode/agents/engineer.md @@ -1,5 +1,5 @@ --- -name: engineer +name: Penpot Engineer description: Senior Full-Stack Software Engineer mode: primary --- diff --git a/.opencode/agents/planner.md b/.opencode/agents/planner.md new file mode 100644 index 0000000000..1236dd7458 --- /dev/null +++ b/.opencode/agents/planner.md @@ -0,0 +1,61 @@ +--- +name: Penpot Planner +description: Software architect for planning and analysis only +mode: primary +permission: + edit: ask +--- + +# Penpot Planner + +## Role + +You are a Senior Software Architect working on Penpot, an open-source design +tool. Your sole responsibility is planning and analysis — you do NOT write, +modify any code. + +You help users understand the codebase, design solutions, and create detailed +implementation plans that other agents or developers can execute. Document +everything they need to know: which files to touch for each task, code, testing, +docs they might need to check, how to test it. Give them the whole plan as +bite-sized tasks. DRY. YAGNI. TDD. Frequent commits. + +Assume they are a skilled developer, but know almost nothing about our toolset +or problem domain. Assume they don't know good test design very well. + +## Requirements + +* Analyze the codebase architecture and identify affected modules. +* Read `AGENTS.md` files (root and per-module) to understand structure and + conventions. +* Search code using `ripgrep` skill (`rg`) to trace dependencies, find patterns, + and understand existing implementations. +* Break down complex features or bugs into atomic, actionable steps. +* Propose solutions with clear rationale, trade-offs, and sequencing. +* Identify risks, edge cases, and testing considerations. + +Save plans to: plans/YYYY-MM-DD-.md + +## Constraints + +* You are **read-only** — never create, edit, or delete files. +* You do **not** run builds, tests, linters, or any commands that modify state. +* You do **not** create git commits or interact with version control. +* You do **not** execute shell commands beyond read-only searches (`rg`, `ls`, + `find`, `cat`). +* Your output is a structured plan or analysis, ready for handoff to an + engineer agent or developer. + +## Output format + +When producing a plan, structure it as: + +1. **Context** — What is the problem or feature request? +2. **Affected modules** — Which parts of the codebase are involved? +3. **Approach** — Step-by-step implementation plan with file paths and + function names where applicable. +4. **Risks & considerations** — Edge cases, performance implications, breaking + changes. +5. **Testing strategy** — How to verify the implementation works correctly. + + diff --git a/.opencode/agents/prompt-assistant.md b/.opencode/agents/prompt-assistant.md new file mode 100644 index 0000000000..fb1f5bee8f --- /dev/null +++ b/.opencode/agents/prompt-assistant.md @@ -0,0 +1,59 @@ +--- +name: Prompt Assistant +description: Refines and improves prompts for maximum clarity and effectiveness +mode: all +--- + +# Prompt Assistant + +## Role + +You are an expert Prompt Engineer with strong knowledge of +penpot. Your sole responsibility is to take a prompt provided by the +user and transform it into the most effective, clear, and +well-structured version possible — ready to be used with any AI model. + +## Requirements + +* You do NOT execute tasks. You do NOT write code. You only design and + refine prompts +* Read the root `AGENTS.md` to understand the repository and application + architecture. Then read the `AGENTS.md` **only** for each affected module. +* Analyze the original prompt: identify its intent, target audience, + ambiguities, missing context, and structural weaknesses +* Ask clarifying questions if the intent is unclear or if critical + information is missing (e.g. target model, expected output format, + tone, constraints). Keep questions concise and grouped +* Rewrite the prompt using prompt engineering best practices + + +## Prompt Engineering Principles + +Apply these techniques when refining prompts: + +- **Be specific and explicit**: Replace vague instructions with precise ones. +- **Set the context**: Include background information the model needs to + perform well. +- **Specify the output format**: State the desired structure, length, tone, + or format (e.g. bullet list, JSON, step-by-step). +- **Add constraints**: Include what the model should avoid or not do. +- **Use examples** (few-shot): When applicable, suggest adding examples to + anchor the model's behaviour. +- **Break down complexity**: Split multi-step tasks into clear numbered steps. +- **Avoid ambiguity**: Remove pronouns and references that could be + misinterpreted. +- **Chain of thought**: For reasoning tasks, include "Think step by step." + +## Constraints + +- Do NOT execute the prompt yourself. +- Do NOT answer the question inside the prompt. +- Do NOT add unnecessary verbosity — prompts should be as short as they can + be while remaining complete. +- Always preserve the user's original intent. + +## Output + +Refined Prompt: The improved, ready-to-use prompt. Print it for +immediate use and save it to +prompts/YYYY-MM-DD-.md for future use. diff --git a/.opencode/agents/testing.md b/.opencode/agents/testing.md deleted file mode 100644 index 17c19aade1..0000000000 --- a/.opencode/agents/testing.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: testing -description: Senior Software Engineer specialized on testing -mode: primary ---- - -Role: You are a Senior Software Engineer specialized in testing Clojure and -ClojureScript codebases. You work on Penpot, an open-source design tool. - -Tech stack: Clojure (backend/JVM), ClojureScript (frontend/Node.js), shared -Cljc (common module), Rust (render-wasm). - -Requirements: - -* Read the root `AGENTS.md` to understand the repository and application - architecture. Then read the `AGENTS.md` **only** for each affected module. Not all - modules have one — verify before reading. -* Before writing code, describe your plan. If the task is complex, break it down into - atomic steps. -* Tests should be exhaustive and include edge cases relevant to Penpot's domain: - nil/missing fields, empty collections, invalid UUIDs, boundary geometries, Malli schema - violations, concurrent state mutations, and timeouts. -* Tests must be deterministic — do not use `setTimeout`, real network calls, or rely on - execution order. Use synchronous mocks for asynchronous workflows. -* Use `with-redefs` or equivalent mocking utilities to isolate the logic under test. Avoid - testing through the UI (DOM); e2e tests cover that. -* Only reference functions, namespaces, or test utilities that actually exist in the - codebase. Verify their existence before citing them. -* After adding or modifying tests, run the applicable lint and format checks for the - affected module before considering the work done (see module `AGENTS.md` for exact - commands). -* Make small and logical commits following the commit guideline described in - `CONTRIBUTING.md`. Commit only when explicitly asked. -- Do not guess or hallucinate git author information (Name or Email). Never include the - `--author` flag in git commands unless specifically instructed by the user for a unique - case; assume the local environment is already configured. Allow git commit to - automatically pull the identity from the local git config `user.name` and `user.email`. diff --git a/.opencode/skills/bat-cat/SKILL.md b/.opencode/skills/bat-cat/SKILL.md new file mode 100644 index 0000000000..61ca8def8f --- /dev/null +++ b/.opencode/skills/bat-cat/SKILL.md @@ -0,0 +1,210 @@ +--- +name: bat-cat +description: A cat clone with syntax highlighting, line numbers, and Git integration - a modern replacement for cat. +homepage: https://github.com/sharkdp/bat +metadata: {"clawdbot":{"emoji":"🦇","requires":{"bins":["bat"]},"install":[{"id":"brew","kind":"brew","formula":"bat","bins":["bat"],"label":"Install bat (brew)"},{"id":"apt","kind":"apt","package":"bat","bins":["bat"],"label":"Install bat (apt)"}]}} +--- + +# bat - Better cat + +`cat` with syntax highlighting, line numbers, and Git integration. + +## Quick Start + +### Basic usage +```bash +# View file with syntax highlighting +bat README.md + +# Multiple files +bat file1.js file2.py + +# With line numbers (default) +bat script.sh + +# Without line numbers +bat -p script.sh +``` + +### Viewing modes +```bash +# Plain mode (like cat) +bat -p file.txt + +# Show non-printable characters +bat -A file.txt + +# Squeeze blank lines +bat -s file.txt + +# Paging (auto for large files) +bat --paging=always file.txt +bat --paging=never file.txt +``` + +## Syntax Highlighting + +### Language detection +```bash +# Auto-detect from extension +bat script.py + +# Force specific language +bat -l javascript config.txt + +# Show all languages +bat --list-languages +``` + +### Themes +```bash +# List available themes +bat --list-themes + +# Use specific theme +bat --theme="Monokai Extended" file.py + +# Set default theme in config +# ~/.config/bat/config: --theme="Dracula" +``` + +## Line Ranges + +```bash +# Show specific lines +bat -r 10:20 file.txt + +# From line to end +bat -r 100: file.txt + +# Start to specific line +bat -r :50 file.txt + +# Multiple ranges +bat -r 1:10 -r 50:60 file.txt +``` + +## Git Integration + +```bash +# Show Git modifications (added/removed/modified lines) +bat --diff file.txt + +# Show decorations (Git + file header) +bat --decorations=always file.txt +``` + +## Output Control + +```bash +# Output raw (no styling) +bat --style=plain file.txt + +# Customize style +bat --style=numbers,changes file.txt + +# Available styles: auto, full, plain, changes, header, grid, numbers, snip +bat --style=header,grid,numbers file.txt +``` + +## Common Use Cases + +**Quick file preview:** +```bash +bat file.json +``` + +**View logs with syntax highlighting:** +```bash +bat error.log +``` + +**Compare files visually:** +```bash +bat --diff file1.txt +bat file2.txt +``` + +**Preview before editing:** +```bash +bat config.yaml && vim config.yaml +``` + +**Cat replacement in pipes:** +```bash +bat -p file.txt | grep "pattern" +``` + +**View specific function:** +```bash +bat -r 45:67 script.py # If function is on lines 45-67 +``` + +## Integration with other tools + +**As pager for man pages:** +```bash +export MANPAGER="sh -c 'col -bx | bat -l man -p'" +man grep +``` + +**With ripgrep:** +```bash +rg "pattern" -l | xargs bat +``` + +**With fzf:** +```bash +fzf --preview 'bat --color=always --style=numbers {}' +``` + +**With diff:** +```bash +diff -u file1 file2 | bat -l diff +``` + +## Configuration + +Create `~/.config/bat/config` for defaults: + +``` +# Set theme +--theme="Dracula" + +# Show line numbers, Git modifications and file header, but no grid +--style="numbers,changes,header" + +# Use italic text on terminal +--italic-text=always + +# Add custom mapping +--map-syntax "*.conf:INI" +``` + +## Performance Tips + +- Use `-p` for plain mode when piping +- Use `--paging=never` when output is used programmatically +- `bat` caches parsed files for faster subsequent access + +## Tips + +- **Alias:** `alias cat='bat -p'` for drop-in cat replacement +- **Pager:** Use as pager with `export PAGER="bat"` +- **On Debian/Ubuntu:** Command may be `batcat` instead of `bat` +- **Custom syntaxes:** Add to `~/.config/bat/syntaxes/` +- **Performance:** For huge files, use `bat --paging=never` or plain `cat` + +## Common flags + +- `-p` / `--plain`: Plain mode (no line numbers/decorations) +- `-n` / `--number`: Only show line numbers +- `-A` / `--show-all`: Show non-printable characters +- `-l` / `--language`: Set language for syntax highlighting +- `-r` / `--line-range`: Only show specific line range(s) + +## Documentation + +GitHub: https://github.com/sharkdp/bat +Man page: `man bat` +Customization: https://github.com/sharkdp/bat#customization diff --git a/.opencode/skills/fd-find/SKILL.md b/.opencode/skills/fd-find/SKILL.md new file mode 100644 index 0000000000..e218ac9bfd --- /dev/null +++ b/.opencode/skills/fd-find/SKILL.md @@ -0,0 +1,194 @@ +--- +name: fd-find +description: A fast and user-friendly alternative to 'find' - simple syntax, smart defaults, respects gitignore. +homepage: https://github.com/sharkdp/fd +metadata: {"clawdbot":{"emoji":"📂","requires":{"bins":["fd"]},"install":[{"id":"brew","kind":"brew","formula":"fd","bins":["fd"],"label":"Install fd (brew)"},{"id":"apt","kind":"apt","package":"fd-find","bins":["fd"],"label":"Install fd (apt)"}]}} +--- + +# fd - Fast File Finder + +User-friendly alternative to `find` with smart defaults. + +## Quick Start + +### Basic search +```bash +# Find files by name +fd pattern + +# Find in specific directory +fd pattern /path/to/dir + +# Case-insensitive +fd -i pattern +``` + +### Common patterns +```bash +# Find all Python files +fd -e py + +# Find multiple extensions +fd -e py -e js -e ts + +# Find directories only +fd -t d pattern + +# Find files only +fd -t f pattern + +# Find symlinks +fd -t l +``` + +## Advanced Usage + +### Filtering +```bash +# Exclude patterns +fd pattern -E "node_modules" -E "*.min.js" + +# Include hidden files +fd -H pattern + +# Include ignored files (.gitignore) +fd -I pattern + +# Search all (hidden + ignored) +fd -H -I pattern + +# Maximum depth +fd pattern -d 3 +``` + +### Execution +```bash +# Execute command on results +fd -e jpg -x convert {} {.}.png + +# Parallel execution +fd -e md -x wc -l + +# Use with xargs +fd -e log -0 | xargs -0 rm +``` + +### Regex patterns +```bash +# Full regex search +fd '^test.*\.js$' + +# Match full path +fd --full-path 'src/.*/test' + +# Glob pattern +fd -g "*.{js,ts}" +``` + +## Time-based filtering +```bash +# Modified within last day +fd --changed-within 1d + +# Modified before specific date +fd --changed-before 2024-01-01 + +# Created recently +fd --changed-within 1h +``` + +## Size filtering +```bash +# Files larger than 10MB +fd --size +10m + +# Files smaller than 1KB +fd --size -1k + +# Specific size range +fd --size +100k --size -10m +``` + +## Output formatting +```bash +# Absolute paths +fd --absolute-path + +# List format (like ls -l) +fd --list-details + +# Null separator (for xargs) +fd -0 pattern + +# Color always/never/auto +fd --color always pattern +``` + +## Common Use Cases + +**Find and delete old files:** +```bash +fd --changed-before 30d -t f -x rm {} +``` + +**Find large files:** +```bash +fd --size +100m --list-details +``` + +**Copy all PDFs to directory:** +```bash +fd -e pdf -x cp {} /target/dir/ +``` + +**Count lines in all Python files:** +```bash +fd -e py -x wc -l | awk '{sum+=$1} END {print sum}' +``` + +**Find broken symlinks:** +```bash +fd -t l -x test -e {} \; -print +``` + +**Search in specific time window:** +```bash +fd --changed-within 2d --changed-before 1d +``` + +## Integration with other tools + +**With ripgrep:** +```bash +fd -e js | xargs rg "pattern" +``` + +**With fzf (fuzzy finder):** +```bash +vim $(fd -t f | fzf) +``` + +**With bat (cat alternative):** +```bash +fd -e md | xargs bat +``` + +## Performance Tips + +- `fd` is typically much faster than `find` +- Respects `.gitignore` by default (disable with `-I`) +- Uses parallel traversal automatically +- Smart case: lowercase = case-insensitive, any uppercase = case-sensitive + +## Tips + +- Use `-t` for type filtering (f=file, d=directory, l=symlink, x=executable) +- `-e` for extension is simpler than `-g "*.ext"` +- `{}` in `-x` commands represents the found path +- `{.}` strips the extension +- `{/}` gets basename, `{//}` gets directory + +## Documentation + +GitHub: https://github.com/sharkdp/fd +Man page: `man fd` diff --git a/.opencode/skills/jq-json-processor/SKILL.md b/.opencode/skills/jq-json-processor/SKILL.md new file mode 100644 index 0000000000..83fe48d7bf --- /dev/null +++ b/.opencode/skills/jq-json-processor/SKILL.md @@ -0,0 +1,112 @@ +--- +name: jq-json-processor +description: Process, filter, and transform JSON data using jq - the lightweight and flexible command-line JSON processor. +homepage: https://jqlang.github.io/jq/ +metadata: {"clawdbot":{"emoji":"🔍","requires":{"bins":["jq"]},"install":[{"id":"brew","kind":"brew","formula":"jq","bins":["jq"],"label":"Install jq (brew)"},{"id":"apt","kind":"apt","package":"jq","bins":["jq"],"label":"Install jq (apt)"}]}} +--- + +# jq JSON Processor + +Process, filter, and transform JSON data with jq. + +## Quick Examples + +### Basic filtering +```bash +# Extract a field +echo '{"name":"Alice","age":30}' | jq '.name' +# Output: "Alice" + +# Multiple fields +echo '{"name":"Alice","age":30}' | jq '{name: .name, age: .age}' + +# Array indexing +echo '[1,2,3,4,5]' | jq '.[2]' +# Output: 3 +``` + +### Working with arrays +```bash +# Map over array +echo '[{"name":"Alice"},{"name":"Bob"}]' | jq '.[].name' +# Output: "Alice" "Bob" + +# Filter array +echo '[1,2,3,4,5]' | jq 'map(select(. > 2))' +# Output: [3,4,5] + +# Length +echo '[1,2,3]' | jq 'length' +# Output: 3 +``` + +### Common operations +```bash +# Pretty print JSON +cat file.json | jq '.' + +# Compact output +cat file.json | jq -c '.' + +# Raw output (no quotes) +echo '{"name":"Alice"}' | jq -r '.name' +# Output: Alice + +# Sort keys +echo '{"z":1,"a":2}' | jq -S '.' +``` + +### Advanced filtering +```bash +# Select with conditions +jq '[.[] | select(.age > 25)]' people.json + +# Group by +jq 'group_by(.category)' items.json + +# Reduce +echo '[1,2,3,4,5]' | jq 'reduce .[] as $item (0; . + $item)' +# Output: 15 +``` + +### Working with files +```bash +# Read from file +jq '.users[0].name' users.json + +# Multiple files +jq -s '.[0] * .[1]' file1.json file2.json + +# Modify and save +jq '.version = "2.0"' package.json > package.json.tmp && mv package.json.tmp package.json +``` + +## Common Use Cases + +**Extract specific fields from API response:** +```bash +curl -s https://api.github.com/users/octocat | jq '{name: .name, repos: .public_repos, followers: .followers}' +``` + +**Convert CSV-like data:** +```bash +jq -r '.[] | [.name, .email, .age] | @csv' users.json +``` + +**Debug API responses:** +```bash +curl -s https://api.example.com/data | jq '.' +``` + +## Tips + +- Use `-r` for raw string output (removes quotes) +- Use `-c` for compact output (single line) +- Use `-S` to sort object keys +- Use `--arg name value` to pass variables +- Pipe multiple jq operations: `jq '.a' | jq '.b'` + +## Documentation + +Full manual: https://jqlang.github.io/jq/manual/ +Interactive tutorial: https://jqplay.org/ diff --git a/.opencode/skills/ripgrep/SKILL.md b/.opencode/skills/ripgrep/SKILL.md new file mode 100644 index 0000000000..31c3a83d5e --- /dev/null +++ b/.opencode/skills/ripgrep/SKILL.md @@ -0,0 +1,150 @@ +--- +name: ripgrep +description: Blazingly fast text search tool - recursively searches directories for regex patterns with respect to gitignore rules. +homepage: https://github.com/BurntSushi/ripgrep +metadata: {"clawdbot":{"emoji":"🔎","requires":{"bins":["rg"]},"install":[{"id":"brew","kind":"brew","formula":"ripgrep","bins":["rg"],"label":"Install ripgrep (brew)"},{"id":"apt","kind":"apt","package":"ripgrep","bins":["rg"],"label":"Install ripgrep (apt)"}]}} +--- + +# ripgrep (rg) + +Fast, smart recursive search. Respects `.gitignore` by default. + +## Quick Start + +### Basic search +```bash +# Search for "TODO" in current directory +rg "TODO" + +# Case-insensitive search +rg -i "fixme" + +# Search specific file types +rg "error" -t py # Python files only +rg "function" -t js # JavaScript files +``` + +### Common patterns +```bash +# Whole word match +rg -w "test" + +# Show only filenames +rg -l "pattern" + +# Show with context (3 lines before/after) +rg -C 3 "function" + +# Count matches +rg -c "import" +``` + +## Advanced Usage + +### File type filtering +```bash +# Multiple file types +rg "error" -t py -t js + +# Exclude file types +rg "TODO" -T md -T txt + +# List available types +rg --type-list +``` + +### Search modifiers +```bash +# Regex search +rg "user_\d+" + +# Fixed string (no regex) +rg -F "function()" + +# Multiline search +rg -U "start.*end" + +# Only show matches, not lines +rg -o "https?://[^\s]+" +``` + +### Path filtering +```bash +# Search specific directory +rg "pattern" src/ + +# Glob patterns +rg "error" -g "*.log" +rg "test" -g "!*.min.js" + +# Include hidden files +rg "secret" --hidden + +# Search all files (ignore .gitignore) +rg "pattern" --no-ignore +``` + +## Replacement Operations + +```bash +# Preview replacements +rg "old_name" --replace "new_name" + +# Actually replace (requires extra tool like sd) +rg "old_name" -l | xargs sed -i 's/old_name/new_name/g' +``` + +## Performance Tips + +```bash +# Parallel search (auto by default) +rg "pattern" -j 8 + +# Skip large files +rg "pattern" --max-filesize 10M + +# Memory map files +rg "pattern" --mmap +``` + +## Common Use Cases + +**Find TODOs in code:** +```bash +rg "TODO|FIXME|HACK" --type-add 'code:*.{rs,go,py,js,ts}' -t code +``` + +**Search in specific branches:** +```bash +git show branch:file | rg "pattern" +``` + +**Find files containing multiple patterns:** +```bash +rg "pattern1" | rg "pattern2" +``` + +**Search with context and color:** +```bash +rg -C 2 --color always "error" | less -R +``` + +## Comparison to grep + +- **Faster:** Typically 5-10x faster than grep +- **Smarter:** Respects `.gitignore`, skips binary files +- **Better defaults:** Recursive, colored output, line numbers +- **Easier:** Simpler syntax for common tasks + +## Tips + +- `rg` is often faster than `grep -r` +- Use `-t` for file type filtering instead of `--include` +- Combine with other tools: `rg pattern -l | xargs tool` +- Add custom types in `~/.ripgreprc` +- Use `--stats` to see search performance + +## Documentation + +GitHub: https://github.com/BurntSushi/ripgrep +User Guide: https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index c9c78b9266..b83ff6a79d 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -308,6 +308,9 @@ RUN set -ex; \ less \ jq \ nginx \ + fd-find \ + bat \ + gh \ \ fontconfig \ woff-tools \ From a5a8ab5de6c3d6916d5b2e97f52a2e4e5c666c25 Mon Sep 17 00:00:00 2001 From: Luis de Dios Date: Mon, 27 Apr 2026 09:37:11 +0200 Subject: [PATCH 4/7] :bug: Fix MCP status is displayed as disabled when setting MCP key without expiration date Fixes #14058 and #14061 in Taiga --- .../src/app/main/ui/workspace/main_menu.cljs | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 2f59df3907..dfa875ad91 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -751,13 +751,14 @@ mcp (mf/deref refs/mcp) tokens (mf/deref refs/access-tokens) - expired? (some->> tokens - (some #(when (= (:type %) "mcp") %)) - :expires-at - (> (ct/now))) + expires-at (some->> tokens + (some #(when (= (:type %) "mcp") %)) + :expires-at) + expired? (and (some? expires-at) (> (ct/now) expires-at)) - mcp-enabled? (true? (-> profile :props :mcp-enabled)) - mcp-connected? (= "connected" (get mcp :connection-status)) + mcp-enabled? (true? (-> profile :props :mcp-enabled)) + mcp-connection (get mcp :connection-status) + mcp-connected? (= mcp-connection "connected") show-enabled? (and mcp-enabled? (false? expired?)) @@ -798,7 +799,7 @@ :pos-6 plugins?) :on-close on-close} - (when show-enabled? + (when (and show-enabled? (not expired?)) [:> dropdown-menu-item* {:id "mcp-menu-toggle-mcp-plugin" :class (stl/css :base-menu-item :submenu-item) :on-click on-toggle-mcp-plugin @@ -988,10 +989,10 @@ (when (contains? cf/flags :mcp) (let [tokens (mf/deref refs/access-tokens) - expired? (some->> tokens - (some #(when (= (:type %) "mcp") %)) - :expires-at - (> (ct/now))) + expires-at (some->> tokens + (some #(when (= (:type %) "mcp") %)) + :expires-at) + expired? (and (some? expires-at) (> (ct/now) expires-at)) mcp-enabled? (true? (-> profile :props :mcp-enabled)) mcp-connection (get mcp :connection-status) @@ -1001,6 +1002,9 @@ active? (and mcp-enabled? mcp-connected?) failed? (or (and mcp-enabled? mcp-error?) (true? expired?))] + + (print "EXPIRED" expired?) + (print "FAILED" mcp-error?) [:> dropdown-menu-item* {:class (stl/css :base-menu-item :menu-item) :on-click on-menu-click From edccda20383b613d16d3af3431964d8c2beab645 Mon Sep 17 00:00:00 2001 From: Luis de Dios Date: Mon, 27 Apr 2026 15:26:55 +0200 Subject: [PATCH 5/7] :bug: Fix remove prints --- frontend/src/app/main/ui/workspace/main_menu.cljs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index dfa875ad91..14c51c67a2 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -1001,10 +1001,7 @@ active? (and mcp-enabled? mcp-connected?) failed? (or (and mcp-enabled? mcp-error?) - (true? expired?))] - - (print "EXPIRED" expired?) - (print "FAILED" mcp-error?) + (true? expired?))] [:> dropdown-menu-item* {:class (stl/css :base-menu-item :menu-item) :on-click on-menu-click From cbd5f7795b3d3ca0ec82b9df98c75edc4b71f829 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 17 Mar 2026 10:39:26 +0100 Subject: [PATCH 6/7] :sparkles: Add minor compatibility adjustments for audit archive task (#8491) --- .gitignore | 1 + backend/scripts/_env | 1 + backend/src/app/config.clj | 1 + backend/src/app/loggers/audit.clj | 2 +- .../src/app/loggers/audit/archive_task.clj | 12 ++--- backend/src/app/main.clj | 11 ++-- backend/src/app/setup.clj | 54 ++++++++----------- common/src/app/common/schema.cljc | 17 +++--- 8 files changed, 48 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index 6a23f2101f..8586839ba0 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,7 @@ /frontend/test-results/ /frontend/.shadow-cljs /other/ +/scripts/ /nexus/ /tmp/ /vendor/**/target diff --git a/backend/scripts/_env b/backend/scripts/_env index 5367a031b9..b475bd2f2b 100644 --- a/backend/scripts/_env +++ b/backend/scripts/_env @@ -2,6 +2,7 @@ export PENPOT_NITRATE_SHARED_KEY=super-secret-nitrate-api-key export PENPOT_EXPORTER_SHARED_KEY=super-secret-exporter-api-key +export PENPOT_NEXUS_SHARED_KEY=super-secret-nexus-api-key export PENPOT_SECRET_KEY=super-secret-devenv-key # DEPRECATED: only used for subscriptions diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index d0a80f6515..a0d680c199 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -103,6 +103,7 @@ [:exporter-shared-key {:optional true} :string] [:nitrate-shared-key {:optional true} :string] + [:nexus-shared-key {:optional true} :string] [:management-api-key {:optional true} :string] [:telemetry-uri {:optional true} :string] diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index c374b432f9..89119b04e1 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -120,7 +120,7 @@ ;; an external storage and data cleared. (def ^:private schema:event - [:map {:title "event"} + [:map {:title "AuditEvent"} [::type ::sm/text] [::name ::sm/text] [::profile-id ::sm/uuid] diff --git a/backend/src/app/loggers/audit/archive_task.clj b/backend/src/app/loggers/audit/archive_task.clj index 4eb87d595e..62024e573b 100644 --- a/backend/src/app/loggers/audit/archive_task.clj +++ b/backend/src/app/loggers/audit/archive_task.clj @@ -10,14 +10,11 @@ [app.common.logging :as l] [app.common.schema :as sm] [app.common.transit :as t] - [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] [app.http.client :as http] [app.setup :as-alias setup] - [app.tokens :as tokens] [integrant.core :as ig] - [lambdaisland.uri :as u] [promesa.exec :as px])) ;; This is a task responsible to send the accumulated events to @@ -52,19 +49,18 @@ (defn- send! [{:keys [::uri] :as cfg} events] - (let [token (tokens/generate cfg - {:iss "authentication" - :uid uuid/zero}) + (let [skey (-> cfg ::setup/shared-keys :nexus) body (t/encode {:events events}) headers {"content-type" "application/transit+json" "origin" (str (cf/get :public-uri)) - "cookie" (u/map->query-string {:auth-token token})} + "x-shared-key" (str "nexus " skey)} params {:uri uri :timeout 12000 :method :post :headers headers :body body} resp (http/req! cfg params)] + (if (= (:status resp) 204) true (do @@ -109,7 +105,7 @@ (def ^:private schema:handler-params [:map ::db/pool - ::setup/props + ::setup/shared-keys ::http/client]) (defmethod ig/assert-key ::handler diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 693752080a..383578531e 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -466,16 +466,17 @@ ::setup/shared-keys {::setup/props (ig/ref ::setup/props) - :nitrate (cf/get :nitrate-shared-key) - :exporter (cf/get :exporter-shared-key)} + :nexus (cf/get :nexus-shared-key) + :nitrate (cf/get :nitrate-shared-key) + :exporter (cf/get :exporter-shared-key)} ::setup/clock {} :app.loggers.audit.archive-task/handler - {::setup/props (ig/ref ::setup/props) - ::db/pool (ig/ref ::db/pool) - ::http.client/client (ig/ref ::http.client/client)} + {::setup/shared-keys (ig/ref ::setup/shared-keys) + ::http.client/client (ig/ref ::http.client/client) + ::db/pool (ig/ref ::db/pool)} :app.loggers.audit.gc-task/handler {::db/pool (ig/ref ::db/pool)} diff --git a/backend/src/app/setup.clj b/backend/src/app/setup.clj index 7406cbca93..2a860f4262 100644 --- a/backend/src/app/setup.clj +++ b/backend/src/app/setup.clj @@ -82,45 +82,37 @@ (db/tx-run! cfg (fn [{:keys [::db/conn]}] (db/xact-lock! conn 0) (when-not key - (l/warn :hint (str "using autogenerated secret-key, it will change on each restart and will invalidate " - "all sessions on each restart, it is highly recommended setting up the " - "PENPOT_SECRET_KEY environment variable"))) + (l/wrn :hint (str "using autogenerated secret-key, it will change " + "on each restart and will invalidate " + "all sessions on each restart, it is highly " + "recommended setting up the " + "PENPOT_SECRET_KEY environment variable"))) (let [secret (or key (generate-random-key))] (-> (get-all-props conn) (assoc :secret-key secret) (assoc :tokens-key (keys/derive secret :salt "tokens")) (update :instance-id handle-instance-id conn (db/read-only? pool))))))) -(sm/register! ::props [:map-of :keyword ::sm/any]) - - (defmethod ig/init-key ::shared-keys [_ {:keys [::props] :as cfg}] (let [secret (get props :secret-key)] - (d/without-nils - {:exporter - (let [key (or (get cfg :exporter) - (-> (keys/derive secret :salt "exporter") - (bc/bytes->b64-str true)))] - (if (or (str/empty? key) - (str/blank? key)) - (do - (l/wrn :hint "exporter key is disabled because empty string found") - nil) - (do - (l/inf :hint "exporter key initialized" :key (d/obfuscate-string key)) - key))) + (reduce (fn [keys id] + (let [key (or (get cfg id) + (-> (keys/derive secret :salt (name id)) + (bc/bytes->b64-str true)))] + (if (or (str/empty? key) + (str/blank? key)) + (do + (l/wrn :id (name id) :hint "key is disabled because empty string found") + keys) + (do + (l/inf :id (name id) :hint "key initialized" :key (d/obfuscate-string key)) + (assoc keys id key))))) + {} + [:exporter + :nitrate + :nexus]))) - :nitrate - (let [key (or (get cfg :nitrate) - (-> (keys/derive secret :salt "nitrate") - (bc/bytes->b64-str true)))] - (if (or (str/empty? key) - (str/blank? key)) - (do - (l/wrn :hint "nitrate key is disabled because empty string found") - nil) - (do - (l/inf :hint "nitrate key initialized" :key (d/obfuscate-string key)) - key)))}))) +(sm/register! ::props [:map-of :keyword ::sm/any]) +(sm/register! ::shared-keys [:map-of :keyword ::sm/text]) diff --git a/common/src/app/common/schema.cljc b/common/src/app/common/schema.cljc index 16bc20d5f4..9c6ec1aa9e 100644 --- a/common/src/app/common/schema.cljc +++ b/common/src/app/common/schema.cljc @@ -5,7 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.common.schema - (:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean type keys]) + (:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean type keys select-keys]) #?(:cljs (:require-macros [app.common.schema :refer [ignoring]])) (:require #?(:clj [malli.dev.pretty :as mdp]) @@ -93,6 +93,11 @@ [& items] (apply mu/merge (map schema items))) +(defn select-keys + [s keys & {:as opts}] + (let [s (schema s)] + (mu/select-keys s keys opts))) + (defn assoc-key "Add a key & value to a schema of type [:map]. If the first level node of the schema is not a map, will do a depth search to find the first map node and add the key there." @@ -138,10 +143,10 @@ (mu/optional-keys schema keys default-options))) (defn required-keys - ([schema] - (mu/required-keys schema nil default-options)) - ([schema keys] - (mu/required-keys schema keys default-options))) + ([s] + (mu/required-keys (schema s) nil default-options)) + ([s keys] + (mu/required-keys (schema s) keys default-options))) (defn transformer [& transformers] @@ -646,7 +651,7 @@ {:title "set" :description "Set of Strings" :error/message "should be a set of strings" - :gen/gen (-> kind sg/generator sg/set) + :gen/gen (sg/mcat (fn [_] (sg/generator kind)) sg/int) :decode/string decode :decode/json decode :encode/string encode-string From a3b9d7bed74efce3b077ab779a98006b25cc0e77 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 27 Apr 2026 17:26:59 +0200 Subject: [PATCH 7/7] :paperclip: Fix fmt issue --- frontend/src/app/main/ui/workspace/main_menu.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 14c51c67a2..28e6f6722c 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -1001,7 +1001,7 @@ active? (and mcp-enabled? mcp-connected?) failed? (or (and mcp-enabled? mcp-error?) - (true? expired?))] + (true? expired?))] [:> dropdown-menu-item* {:class (stl/css :base-menu-item :menu-item) :on-click on-menu-click