Skip to content

feat(brand-context): add org-scoped brand context with MCP prompt#3021

Merged
viktormarinho merged 30 commits intomainfrom
viktormarinho/brand-context
Apr 7, 2026
Merged

feat(brand-context): add org-scoped brand context with MCP prompt#3021
viktormarinho merged 30 commits intomainfrom
viktormarinho/brand-context

Conversation

@viktormarinho
Copy link
Copy Markdown
Contributor

@viktormarinho viktormarinho commented Apr 6, 2026

What is this contribution about?

Adds a Brand Context feature — an org-scoped company profile (name, domain, overview, colors, fonts, logos) that gets exposed as an MCP prompt (/company-context) so any connected AI client knows who the company is.

What's included:

  • Database: Migration 064 creates brand_context table with organization_id as PK
  • Storage: BrandContextStorage class with get/upsert/delete, JSON parsing for fonts/colors/images
  • Tools: BRAND_CONTEXT_GET and BRAND_CONTEXT_UPDATE via defineTool()
  • MCP Prompt: Dynamic company-context prompt registered in managementMCP() — formats brand data into a structured markdown message
  • Settings UI: New page at /$org/settings/brand-context with always-editable form, structured font rows (family/weight/style), color key-value pairs with native color picker, logo URL inputs
  • Wiring: Registry metadata, query keys, sidebar entry ("Context" with BookOpen01 icon), route registration, test mock updates

How to Test

  1. Run bun run dev (migration 064 will auto-apply)
  2. Navigate to Settings > Context in any org
  3. Fill in company name, domain, overview, add colors/fonts, save
  4. In an MCP client connected to the org, invoke /company-context — verify the brand data appears as a formatted prompt
  5. Verify BRAND_CONTEXT_GET / BRAND_CONTEXT_UPDATE work via the management MCP

Migration Notes

  • Migration 064-brand-context creates the brand_context table — runs automatically on startup

Review Checklist

  • PR title is clear and descriptive
  • Changes are tested and working (bun run check + bun test pass)
  • No breaking changes

Summary by cubic

Adds an org-scoped Brand Context with per-brand MCP prompts (/brand-<slug>) and a multi-brand settings page (create/edit/preview/extract/archive). Removes the get_brand_context built-in tool in favor of the management MCP tools.

  • New Features

    • Storage: BrandContextStorage with list/get/create/update/delete/getDefault/setDefault; all ops require organizationId. list filters archived by default (opt-in includeArchived). Adds isDefault.
    • Tools: BRAND_CONTEXT_LIST/GET/CREATE/UPDATE/DELETE/EXTRACT; all verify org ownership. LIST supports includeArchived and returns archivedAt; UPDATE can unarchive via archivedAt: null and set default; DELETE archives. Extractor maps fonts/colors/logos, derives name from the shortest title segment, stores rich tokens in metadata, and reads the Firecrawl API key from settings.
    • MCP: one prompt per brand using a name slug (e.g. /brand-acme), falling back to ID only when the name is empty; archived brands are skipped. Slug collisions are de-duplicated by appending an ID suffix. Prompt accepts colors as {label,value}[] or Record<string,string>, plus legacy font shapes.
    • Settings UI: /$org/settings/brand-context with collapsible brand cards (preview, hover-to-edit sections with Save/Cancel), Add Brand, per-brand Archive/Unarchive with an archived toggle, a star to set the default brand (highlighted border when default), and an Auto-extract banner wired to BRAND_CONTEXT_EXTRACT. Guards against non-array tool responses.
  • Bug Fixes

    • Extract now refreshes the brand list immediately (removed query staleTime, invalidates with refetchType: "all"), and unwraps tool results to reliably detect and report errors.
    • Prevent duplicate extract on Enter, mark BRAND_CONTEXT_DELETE as destructive, and make default-setting transactional to avoid race conditions.
    • Allow removing default by passing isDefault: false in BRAND_CONTEXT_UPDATE, and clear default on archive in BRAND_CONTEXT_DELETE.
    • Minor UI cleanup using cn() for conditional class names.

Written for commit 4e9aa8d. Summary will update on new commits.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 6, 2026

🧪 Benchmark

Should we run the Virtual MCP strategy benchmark for this PR?

React with 👍 to run the benchmark.

Reaction Action
👍 Run quick benchmark (10 & 128 tools)

Benchmark will run on the next push after you react.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 6, 2026

Release Options

Suggested: Minor (2.243.0) — based on feat: prefix

React with an emoji to override the release type:

Reaction Type Next Version
👍 Prerelease 2.242.7-alpha.1
🎉 Patch 2.242.7
❤️ Minor 2.243.0
🚀 Major 3.0.0

Current version: 2.242.6

Note: If multiple reactions exist, the smallest bump wins. If no reactions, the suggested bump is used (default: patch).

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 22 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/web/views/settings/org-brand-context.tsx">

<violation number="1" location="apps/mesh/src/web/views/settings/org-brand-context.tsx:124">
P1: The form state is only initialized once, so later `brandContext` updates are ignored and inputs can become stale.

(Based on your team's feedback about syncing draft input state when async server values change.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/mesh/migrations/064-brand-context.ts">

<violation number="1" location="apps/mesh/migrations/064-brand-context.ts:6">
P1: Add a foreign key from `brand_context.organization_id` to `organization.id` with cascade delete to preserve org-scoped referential integrity.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/mesh/src/web/views/settings/org-brand-context.tsx Outdated
Comment thread apps/mesh/migrations/064-brand-context.ts Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 11 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/tools/organization/brand-context-update.ts">

<violation number="1" location="apps/mesh/src/tools/organization/brand-context-update.ts:82">
P1: `BRAND_CONTEXT_UPDATE` is missing organization ownership validation before updating by `id`, enabling cross-org modification when an external id is provided.</violation>

<violation number="2" location="apps/mesh/src/tools/organization/brand-context-update.ts:139">
P1: `BRAND_CONTEXT_DELETE` deletes by raw `id` without checking the active organization, which allows cross-org deletion.</violation>
</file>

<file name="apps/mesh/src/tools/organization/brand-context-get.ts">

<violation number="1" location="apps/mesh/src/tools/organization/brand-context-get.ts:75">
P1: `BRAND_CONTEXT_GET` is missing organization scoping and can return brand contexts from other organizations by ID.</violation>
</file>

<file name="apps/mesh/src/storage/brand-context.ts">

<violation number="1" location="apps/mesh/src/storage/brand-context.ts:55">
P1: Brand context get/update/delete are not tenant-scoped, allowing cross-organization access by ID.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/mesh/src/tools/organization/brand-context-update.ts Outdated
Comment thread apps/mesh/src/tools/organization/brand-context-update.ts Outdated
Comment thread apps/mesh/src/tools/organization/brand-context-get.ts Outdated
Comment thread apps/mesh/src/storage/brand-context.ts
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/tools/index.ts">

<violation number="1" location="apps/mesh/src/tools/index.ts:270">
P2: Prompt names can collide when multiple brand contexts share the same name, causing one prompt to overwrite the other. Include a unique suffix (e.g., brand.id) in the prompt name to keep all brand prompts accessible.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/mesh/src/tools/index.ts Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/web/views/settings/org-brand-context.tsx">

<violation number="1" location="apps/mesh/src/web/views/settings/org-brand-context.tsx:571">
P2: Avoid auto-saving image URL inputs on every keystroke; keep a local draft and persist on blur so partial URLs don’t trigger repeated mutations.

(Based on your team's feedback about saving URL inputs only on blur.) [FEEDBACK_USED]</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/mesh/src/web/views/settings/org-brand-context.tsx Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/web/views/settings/org-brand-context.tsx">

<violation number="1" location="apps/mesh/src/web/views/settings/org-brand-context.tsx:301">
P2: Header previews use `brand` props instead of local `colors`/`fonts` state, so the collapsed card can show stale values after edits until a refetch finishes.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/mesh/src/web/views/settings/org-brand-context.tsx Outdated
@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai bot commented Apr 7, 2026

You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment @cubic-dev-ai review.

@viktormarinho
Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai review

@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai bot commented Apr 7, 2026

@cubic-dev-ai review

@viktormarinho I have started the AI code review. It will take a few minutes to complete.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 22 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/web/views/settings/org-brand-context.tsx">

<violation number="1" location="apps/mesh/src/web/views/settings/org-brand-context.tsx:283">
P2: Clear `timerRef.current` after the debounced save runs; otherwise props-to-state sync is permanently disabled after the first autosave.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/mesh/src/web/views/settings/org-brand-context.tsx Outdated
viktormarinho and others added 15 commits April 7, 2026 14:27
Add a brand context feature that lets organizations define their company
profile (name, domain, overview, colors, fonts, logos) which is exposed
as an MCP prompt (/company-context) for AI clients.

- Migration 064: brand_context table with organization_id PK
- BrandContextStorage with get/upsert/delete
- BRAND_CONTEXT_GET and BRAND_CONTEXT_UPDATE tools
- Dynamic company-context MCP prompt in managementMCP()
- Settings UI at /$org/settings/brand-context with structured inputs
- Color picker, font rows, always-editable form

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…s on save

- Add .references("organization.id").onDelete("cascade") to migration
- Pass images through in UI mutation to prevent silent data loss

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change brand_context from 1:1 (org_id as PK) to N:1 (UUID id as PK,
org_id as indexed FK). This lets orgs manage multiple brand profiles.

- Migration 064: id column as PK, organization_id as FK with index
- Storage: list(orgId), get(id), create(), update(id), delete(id)
- Tools: BRAND_CONTEXT_LIST, GET, CREATE, UPDATE, DELETE
- MCP prompt: concatenates all brands for the org
- UI: list of brand cards with Add/Delete, per-card Save

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of a single /company-context prompt, register /brand-<slug>
for each brand (e.g. /brand-acme-corp). The slug is derived from
the brand name. Each prompt contains only that brand's context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use unwrapToolResult to properly extract structuredContent from MCP
tool response. Remove staleTime so invalidation triggers immediate
refetch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…chema

Migration 065 handles DBs that already ran the old 064 (organization_id
as PK, no id column). Detects whether the id column exists and only
alters the table if needed — fresh DBs with the new 064 are a no-op.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove manual Save button. Each field change schedules an auto-save
after 1 second of inactivity. Shows "Saving..." indicator in the
card header while persisting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…uto-save

- Logo/Favicon/OG Image fields now show an image preview above the URL
  input when a valid URL is set
- Fix color picker onChange that was using setColors instead of
  updateColors (skipping auto-save)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Brand cards start collapsed showing logo thumbnail, name, domain,
color swatches, and font names at a glance. Click to expand into
the full editable form. Delete button stays visible in both states.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a metadata text column to brand_context for storing extra Firecrawl
data (typography, components, spacing, layout, tone, etc.) that can be
normalized into dedicated columns later.

- Migration 066 adds column to existing DBs, 064 includes it for fresh
- Threaded through storage, tools, schema, and UI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Collapse migrations 064+065+066 into one migration with the final
schema (id PK, organization_id FK with index, metadata column).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- GET/UPDATE/DELETE now verify brand belongs to caller's organization
  before operating, preventing cross-org access via guessed UUIDs
- UPDATE handler uses `!== undefined` pattern for logo/favicon/ogImage
  so explicit null (clear field) reaches the storage layer
- Migration uses sql`now()` instead of string literal "now()"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BrandCard useState initializers only run on mount — after query
refetch the local state could be stale. Track brand.updatedAt via
ref and re-sync all fields when it changes, skipping if a debounced
save is pending to avoid clobbering in-flight edits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… live preview

- Storage get/update/delete now require organizationId parameter,
  enforcing tenant isolation at the data layer (defense-in-depth)
- Prompt names include brand ID suffix to prevent collision when
  multiple brands share the same name
- ImageField saves on blur instead of every keystroke to avoid
  spamming mutations with partial URLs
- Collapsed card header uses local state (name, domain, logo, colors,
  fonts) instead of brand props for instant feedback after edits

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@viktormarinho viktormarinho force-pushed the viktormarinho/brand-context branch from b147c68 to 7915cf3 Compare April 7, 2026 17:27
viktormarinho and others added 2 commits April 7, 2026 14:35
Use the reviewed card-based design with hover-to-edit pencil buttons,
save/cancel inline controls, auto-extract banner, checkerboard logo
previews, color palette circles, and font "Aa" previews.

Adapts for multi-brand backend: uses BRAND_CONTEXT_LIST to get first
brand, CREATE on first save, UPDATE with id for subsequent saves.
MCP prompt handles both {label,value}[] and Record<k,v> color formats.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
List all brands as expandable entries. Collapsed state shows logo
thumbnail (checkerboard bg), name, domain, color swatches, and font
names. Clicking expands to the full reviewed section cards (Overview,
Logos, Fonts, Colors) with hover-to-edit. Add Brand button at top,
per-brand delete on hover.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/web/views/settings/org-brand-context.tsx">

<violation number="1" location="apps/mesh/src/web/views/settings/org-brand-context.tsx:662">
P2: Avoid nesting an interactive delete control inside the header <button>. This is invalid HTML and can cause keyboard/assistive interactions to fail. Move the delete action outside the button or make the header a non-button container with proper keyboard handling.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

return (
<div className="rounded-2xl border border-border/60 bg-background">
{/* Collapsed header — always visible */}
<button
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Avoid nesting an interactive delete control inside the header . This is invalid HTML and can cause keyboard/assistive interactions to fail. Move the delete action outside the button or make the header a non-button container with proper keyboard handling.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/views/settings/org-brand-context.tsx, line 662:

<comment>Avoid nesting an interactive delete control inside the header <button>. This is invalid HTML and can cause keyboard/assistive interactions to fail. Move the delete action outside the button or make the header a non-button container with proper keyboard handling.</comment>

<file context>
@@ -599,6 +603,183 @@ function ColorsSection({
+  return (
+    <div className="rounded-2xl border border-border/60 bg-background">
+      {/* Collapsed header — always visible */}
+      <button
+        type="button"
+        className="flex w-full items-center gap-3 p-5"
</file context>
Fix with Cubic

viktormarinho and others added 3 commits April 7, 2026 14:43
Use Array.isArray check on unwrapped result to prevent
"brands.map is not a function" when the response shape
is unexpected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ID suffix (e.g. brand-deco-6da413dd) was showing in the MCP
client UI as "Brand Deco 6da413dd". Drop it — use just the slug
(brand-deco). Falls back to brand ID only when name is empty.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New tool that scrapes a website via Firecrawl's branding endpoint and
saves the extracted brand context (colors, fonts, logos, OG image,
favicon, typography, and metadata).

- Add FIRECRAWL_API_KEY to Settings
- Map Firecrawl BrandingProfile to our schema (colors as {label,value}[],
  fonts as {name,role}[], rich tokens in metadata)
- Derive company name from OG metadata, overview from meta description
- Creates new brand or updates existing (via optional brandId param)
- Wire UI Extract button to call the tool with loading state
- Update colors Zod schema to accept both array and object formats

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 9 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/web/views/settings/org-brand-context.tsx">

<violation number="1" location="apps/mesh/src/web/views/settings/org-brand-context.tsx:149">
P2: The Enter key handler still fires while `isExtracting` is true, so users can trigger multiple extract calls even though the button is disabled. Add an `!isExtracting` guard to the key handler to match the button’s disabled state.</violation>
</file>

<file name="apps/mesh/src/tools/organization/brand-context-extract.ts">

<violation number="1" location="apps/mesh/src/tools/organization/brand-context-extract.ts:179">
P2: Treat empty derived titles as missing so the brand name falls back to the domain. Using `??` here allows an empty string to be saved as the name.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/mesh/src/web/views/settings/org-brand-context.tsx
Comment thread apps/mesh/src/tools/organization/brand-context-extract.ts
The previous logic used ogSiteName which often contains the full
tagline. Now picks the shortest segment after splitting the page
title on separators (|, –, —), which is typically the brand name
(e.g. "Visual CMS for Your Storefront | Deco" → "Deco").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/tools/organization/brand-context-extract.ts">

<violation number="1" location="apps/mesh/src/tools/organization/brand-context-extract.ts:182">
P2: The new title split regex no longer handles the common "-" separator, so titles like "Acme - Home" won’t be split and the extracted name becomes the full title. Re-include hyphen in the separator set to avoid this regression.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

// in the title (e.g. "Visual CMS for Your Storefront | Deco" → "Deco"),
// then ogSiteName, then the domain as last resort.
const titleParts = (metadata.title as string)
?.split(/[|–—]/)
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new title split regex no longer handles the common "-" separator, so titles like "Acme - Home" won’t be split and the extracted name becomes the full title. Re-include hyphen in the separator set to avoid this regression.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/tools/organization/brand-context-extract.ts, line 182:

<comment>The new title split regex no longer handles the common "-" separator, so titles like "Acme - Home" won’t be split and the extracted name becomes the full title. Re-include hyphen in the separator set to avoid this regression.</comment>

<file context>
@@ -175,14 +175,18 @@ export const BRAND_CONTEXT_EXTRACT = defineTool({
+    // in the title (e.g. "Visual CMS for Your Storefront | Deco" → "Deco"),
+    // then ogSiteName, then the domain as last resort.
+    const titleParts = (metadata.title as string)
+      ?.split(/[|–—]/)
+      .map((s) => s.trim())
+      .filter(Boolean);
</file context>
Suggested change
?.split(/[|]/)
?.split(/[|\-]/)
Fix with Cubic

Replace hard delete with archive (sets archived_at). Archived brands
are filtered from the default list and don't generate MCP prompts.

- Add archived_at nullable timestamptz column to migration 064
- Storage list() filters out archived by default, accepts includeArchived option
- BRAND_CONTEXT_DELETE now sets archived_at instead of deleting the row
- UI shows "Brand archived" toast

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/tools/organization/brand-context-update.ts">

<violation number="1" location="apps/mesh/src/tools/organization/brand-context-update.ts:143">
P2: `BRAND_CONTEXT_DELETE` is marked as non-destructive even though it archives records; this misclassifies the operation for clients that rely on tool safety hints.</violation>

<violation number="2" location="apps/mesh/src/tools/organization/brand-context-update.ts:174">
P2: Archiving always overwrites `archivedAt`, so repeated delete/archive calls lose the original archival timestamp.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/mesh/src/tools/organization/brand-context-update.ts
Comment thread apps/mesh/src/tools/organization/brand-context-update.ts Outdated
viktormarinho and others added 2 commits April 7, 2026 15:17
Fetch all brands (including archived) and split into active/archived.
Active brands render normally. A subtle "N archived" toggle at the
bottom reveals archived brands at reduced opacity. Archived entries
show an unarchive action instead of archive.

- LIST tool accepts includeArchived param
- archivedAt added to schema, serialized in all tool responses
- UPDATE tool supports archivedAt field for unarchive (set to null)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add is_default boolean to brand_context. When a brand is set as default,
a get_brand_context built-in tool is available in every agent session.
The agent calls it when brand info is relevant — no system prompt bloat.

- Migration 064: add is_default column (boolean, default false)
- Storage: getDefault(orgId), setDefault(id, orgId) clears other defaults
- Built-in tool: get_brand_context returns default brand data (name,
  domain, overview, colors, fonts, logos)
- Registered in decopilot built-in tools index alongside agent_search etc
- UI: star icon on brand cards — filled star for default, click to set
- Default brand card has a subtle primary border

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 9 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/storage/brand-context.ts">

<violation number="1" location="apps/mesh/src/storage/brand-context.ts:182">
P1: `setDefault` clears and sets defaults in separate non-atomic writes, which can leave zero or multiple defaults under failures/concurrency.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/mesh/src/storage/brand-context.ts Outdated
viktormarinho and others added 5 commits April 7, 2026 15:55
1. BRAND_CONTEXT_EXTRACT reads firecrawlApiKey from ctx instead of
   getSettings() — follows CLAUDE.md rule that tools never access
   env vars directly
2. Prompt registration deduplicates names — if two brands slugify to
   the same name, appends ID suffix to prevent crash
3. UI sends null (not undefined) when clearing logo/favicon/ogImage/
   fonts/colors so the update handler correctly clears the field

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove staleTime on brand list query so invalidation triggers
  immediate refetch
- Use refetchType: "all" on invalidation
- Unwrap tool result in extract mutation to properly detect errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The agent already has access to brand data via BRAND_CONTEXT_LIST
from the management MCP. The built-in tool was redundant and added
confusion (two tools for the same thing).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- AutoExtractBanner Enter key handler now checks !isExtracting to
  prevent duplicate extractions while one is in flight
- BRAND_CONTEXT_DELETE (archive) correctly marked as destructiveHint
- setDefault uses a transaction to atomically clear+set, preventing
  zero or multiple defaults under concurrent writes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/storage/brand-context.ts">

<violation number="1" location="apps/mesh/src/storage/brand-context.ts:181">
P1: `setDefault` can clear the current default and still throw when the target id is invalid, leaving the org with no default. Validate affected rows (and throw) inside the transaction so it rolls back.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +181 to +201
await this.db.transaction().execute(async (trx) => {
// Clear all defaults for this org
await trx
.updateTable("brand_context")
.set({ is_default: false })
.where("organization_id", "=", organizationId)
.where("is_default", "=", true)
.execute();

// Set the new default
await trx
.updateTable("brand_context")
.set({ is_default: true, updated_at: new Date().toISOString() })
.where("id", "=", id)
.where("organization_id", "=", organizationId)
.execute();
});

const result = await this.get(id, organizationId);
if (!result) throw new Error("Brand context not found");
return result;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: setDefault can clear the current default and still throw when the target id is invalid, leaving the org with no default. Validate affected rows (and throw) inside the transaction so it rolls back.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/storage/brand-context.ts, line 181:

<comment>`setDefault` can clear the current default and still throw when the target id is invalid, leaving the org with no default. Validate affected rows (and throw) inside the transaction so it rolls back.</comment>

<file context>
@@ -178,16 +178,27 @@ export class BrandContextStorage implements BrandContextStoragePort {
-      .where("organization_id", "=", organizationId)
-      .where("is_default", "=", true)
-      .execute();
+    await this.db.transaction().execute(async (trx) => {
+      // Clear all defaults for this org
+      await trx
</file context>
Suggested change
await this.db.transaction().execute(async (trx) => {
// Clear all defaults for this org
await trx
.updateTable("brand_context")
.set({ is_default: false })
.where("organization_id", "=", organizationId)
.where("is_default", "=", true)
.execute();
// Set the new default
await trx
.updateTable("brand_context")
.set({ is_default: true, updated_at: new Date().toISOString() })
.where("id", "=", id)
.where("organization_id", "=", organizationId)
.execute();
});
const result = await this.get(id, organizationId);
if (!result) throw new Error("Brand context not found");
return result;
const result = await this.db.transaction().execute(async (trx) => {
// Clear all defaults for this org
await trx
.updateTable("brand_context")
.set({ is_default: false })
.where("organization_id", "=", organizationId)
.where("is_default", "=", true)
.execute();
// Set the new default
const setResult = await trx
.updateTable("brand_context")
.set({ is_default: true, updated_at: new Date().toISOString() })
.where("id", "=", id)
.where("organization_id", "=", organizationId)
.executeTakeFirst();
if (Number(setResult.numUpdatedRows ?? 0) === 0) {
throw new Error("Brand context not found");
}
const record = await trx
.selectFrom("brand_context")
.selectAll()
.where("id", "=", id)
.where("organization_id", "=", organizationId)
.executeTakeFirst();
if (!record) throw new Error("Brand context not found");
return toEntity(record);
});
return result;
Fix with Cubic

- UPDATE handler now passes isDefault:false to storage when explicitly
  set, allowing programmatic removal of default status
- BRAND_CONTEXT_DELETE (archive) clears is_default alongside setting
  archivedAt, preventing stale defaults after unarchive

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@viktormarinho viktormarinho merged commit 04f7db9 into main Apr 7, 2026
15 checks passed
@viktormarinho viktormarinho deleted the viktormarinho/brand-context branch April 7, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant