Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ Create a team of specialized AI agents, each with its own personality, LLM, skil

| Skill | Description |
|-------|-------------|
| citedy-seo-agent | Full-stack SEO agent with 59 tools |
| citedy-seo-agent | Full-stack SEO agent with 70+ marketing tools |
| citedy-content-writer | Blog autopilot — articles, illustrations, voice-over |
| citedy-content-ingestion | Ingest YouTube, PDFs, web pages, audio |
| citedy-trend-scout | Scout X/Twitter and Reddit for trends |
Expand Down
15 changes: 12 additions & 3 deletions console/src/layouts/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ export default function Sidebar({ selectedKey }: SidebarProps) {
const [citedyBalance, setCitedyBalance] = useState<{
configured: boolean;
credits?: number;
status?: string;
billing_url?: string;
developer_url?: string;
} | null>(null);
const collapsed = isNarrowViewport || desktopCollapsed;
const useCompactPopupMenu = isNarrowViewport && collapsed;
Expand All @@ -93,7 +95,9 @@ export default function Sidebar({ selectedKey }: SidebarProps) {
setCitedyBalance({
configured: res.configured,
credits: res.balance?.credits,
status: res.status,
billing_url: res.billing_url,
developer_url: res.developer_url,
});
})
.catch(() => {});
Expand Down Expand Up @@ -372,6 +376,8 @@ export default function Sidebar({ selectedKey }: SidebarProps) {
<Wallet size={14} />
{citedyBalance.credits != null
? `${citedyBalance.credits} credits`
: citedyBalance.status === "invalid"
? "Reconnect Citedy"
: "Citedy"}
</span>
<Button
Expand All @@ -381,13 +387,16 @@ export default function Sidebar({ selectedKey }: SidebarProps) {
icon={<ExternalLink size={12} />}
onClick={() =>
window.open(
citedyBalance.billing_url ||
"https://www.citedy.com/dashboard/billing",
citedyBalance.status === "invalid"
? citedyBalance.developer_url ||
"https://www.citedy.com/developer"
: citedyBalance.billing_url ||
"https://www.citedy.com/dashboard/billing",
"_blank",
)
}
>
Top Up
{citedyBalance.status === "invalid" ? "Fix" : "Top Up"}
</Button>
</div>
</div>
Expand Down
30 changes: 27 additions & 3 deletions console/src/pages/Agent/MCP/components/MCPClientCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ interface MCPClientCardProps {
onMouseLeave: () => void;
}

const ACRONYMS: Record<string, string> = {
ai: "AI",
api: "API",
ga4: "GA4",
gsc: "GSC",
llm: "LLM",
mcp: "MCP",
seo: "SEO",
xai: "xAI",
};

function normalizeMcpDisplayName(rawName: string): string {
const normalized = rawName.trim().replace(/[_\s-]?mcp$/i, "");
const words = normalized.split(/[_\s-]+/).filter(Boolean);
const title = words
.map((word) => {
const lower = word.toLowerCase();
return ACRONYMS[lower] || lower.charAt(0).toUpperCase() + lower.slice(1);
})
.join(" ");
return title ? `${title} MCP` : rawName;
}

export function MCPClientCard({
client,
onToggle,
Expand All @@ -31,6 +54,7 @@ export function MCPClientCard({
const [editedJson, setEditedJson] = useState("");
const [editedDescription, setEditedDescription] = useState("");
const [isEditing, setIsEditing] = useState(false);
const displayName = normalizeMcpDisplayName(client.name || client.key);

// Determine if MCP client is remote or local based on command
const isRemote =
Expand Down Expand Up @@ -96,8 +120,8 @@ export function MCPClientCard({
<span className={styles.fileIcon}>
<Server style={{ color: "#3b82f6", fontSize: 20 }} />
</span>
<Tooltip title={client.name}>
<h3 className={styles.mcpTitle}>{client.name}</h3>
<Tooltip title={`Config name: ${client.name}`}>
<h3 className={styles.mcpTitle}>{displayName}</h3>
</Tooltip>
<span
className={`${styles.typeBadge} ${
Expand Down Expand Up @@ -176,7 +200,7 @@ export function MCPClientCard({
</Modal>

<Modal
title={`${client.name} - Configuration`}
title={`${displayName} - Configuration`}
open={jsonModalOpen}
onCancel={() => setJsonModalOpen(false)}
footer={
Expand Down
33 changes: 26 additions & 7 deletions console/src/pages/Agent/MCP/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ type NormalizedMCPClientPayload = {
interface CitedyStatus {
configured: boolean;
api_key_prefix?: string;
status?: string;
balance?: { credits: number; status: string } | null;
error?: string;
developer_url: string;
billing_url: string;
}
Expand Down Expand Up @@ -90,6 +92,20 @@ function normalizeClientData(
};
}

function citedyStatusLine(status: CitedyStatus): string {
if (!status.configured) {
return "API key not configured — connect Citedy to unlock 70+ marketing tools";
}
const keyLabel = `API key: ${status.api_key_prefix || "configured"}`;
if (status.balance) {
return `${keyLabel} | Balance: ${status.balance.credits} credits`;
}
if (status.status === "invalid") {
return `${keyLabel} | Reconnect required — balance unavailable`;
}
return `${keyLabel} | Balance unavailable`;
}

function MCPPage() {
const { t } = useTranslation();
const {
Expand Down Expand Up @@ -254,11 +270,7 @@ function MCPPage() {
</a>
</h3>
<p style={{ margin: "4px 0 0", color: "#475569", fontSize: 13 }}>
{citedyStatus.configured
? `API Key: ${citedyStatus.api_key_prefix || "configured"}`
: "API key not configured — get a free key to unlock 59 marketing tools"}
{citedyStatus.balance &&
` | Balance: ${citedyStatus.balance.credits} credits`}
{citedyStatusLine(citedyStatus)}
</p>
</div>
<div style={{ display: "flex", gap: 8 }}>
Expand All @@ -275,10 +287,17 @@ function MCPPage() {
{citedyStatus.configured && (
<Button
onClick={() =>
window.open(citedyStatus.billing_url, "_blank")
window.open(
citedyStatus.status === "invalid"
? citedyStatus.developer_url
: citedyStatus.billing_url,
"_blank",
)
}
>
Top Up Balance
{citedyStatus.status === "invalid"
? "Reconnect Key"
: "Top Up Balance"}
</Button>
)}
</div>
Expand Down
8 changes: 5 additions & 3 deletions console/src/pages/Agent/Skills/components/SecurityBadges.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,24 @@ const scoreColor = (score: number) => {

interface Props {
security?: SkillSecurity;
source?: string;
}

export function SecurityBadges({ security }: Props) {
export function SecurityBadges({ security, source }: Props) {
if (!security) {
const isBuiltIn = source === "builtin";
return (
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
fontSize: 12,
color: "#cbd5e1",
color: isBuiltIn ? "#64748b" : "#94a3b8",
marginTop: 8,
}}
>
<span>Not scanned</span>
<span>{isBuiltIn ? "Built-in verified" : "Scan available"}</span>
</div>
);
}
Expand Down
13 changes: 9 additions & 4 deletions console/src/pages/Agent/Skills/components/SkillCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ interface SkillCardProps {
}

const MAX_DESC_LEN = 90;
const SOURCE_LABELS: Record<string, string> = {
builtin: "Built-in",
customized: "Customized",
active: "Active",
};

function extractDescription(content: string): string {
// Parse description from YAML frontmatter: "description: ..." or multi-line "description: >\n ..."
Expand Down Expand Up @@ -171,9 +176,9 @@ export function SkillCard({
</div>
</div>

<div className={styles.infoSection}>
<div className={styles.infoLabel}>{t("skills.source")}</div>
<code className={styles.infoCode}>{skill.source}</code>
<div className={styles.sourcePill}>
<span>{t("skills.source")}</span>
<strong>{SOURCE_LABELS[skill.source] || skill.source}</strong>
</div>

<div className={styles.infoSection}>
Expand All @@ -185,7 +190,7 @@ export function SkillCard({
</span>
</div>

<SecurityBadges security={skill.security} />
<SecurityBadges security={skill.security} source={skill.source} />
</div>

<div className={styles.cardFooter}>
Expand Down
20 changes: 20 additions & 0 deletions console/src/pages/Agent/Skills/index.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,26 @@
height: 50px;
}

.sourcePill {
display: inline-flex;
align-items: center;
gap: 6px;
width: fit-content;
margin-bottom: 12px;
padding: 4px 8px;
border: 1px solid rgba(148, 163, 184, 0.28);
border-radius: 9999px;
background: rgba(248, 250, 252, 0.86);
color: var(--citedy-slate-500);
font-size: 12px;
line-height: 1;

strong {
color: var(--citedy-slate-700);
font-weight: 650;
}
}

.infoLabel {
font-size: 12px;
color: var(--citedy-slate-500);
Expand Down
8 changes: 5 additions & 3 deletions console/src/pages/Agent/Skills/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,13 @@ function SkillsPage() {
})
.map((skill) => (
<SkillCard
key={skill.name}
key={`${skill.source}:${skill.name}`}
skill={skill}
isHover={hoverKey === skill.name}
isHover={hoverKey === `${skill.source}:${skill.name}`}
onClick={() => handleEdit(skill)}
onMouseEnter={() => setHoverKey(skill.name)}
onMouseEnter={() =>
setHoverKey(`${skill.source}:${skill.name}`)
}
onMouseLeave={() => setHoverKey(null)}
onToggleEnabled={(e) => handleToggleEnabled(skill, e)}
onDelete={(e) => handleDelete(skill, e)}
Expand Down
23 changes: 21 additions & 2 deletions src/adclaw/app/routers/citedy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging
import os
from urllib.request import Request, urlopen
from urllib.error import URLError
from urllib.error import HTTPError, URLError
import json

from fastapi import APIRouter, HTTPException
Expand All @@ -18,6 +18,7 @@
CITEDY_API_BASE = "https://www.citedy.com"
CITEDY_DEVELOPER_URL = "https://www.citedy.com/developer"
CITEDY_BILLING_URL = "https://www.citedy.com/dashboard/billing"
CITEDY_MCP_DESCRIPTION = "Citedy SEO & Marketing Tools (70+ tools)"


def _get_citedy_api_key() -> str:
Expand Down Expand Up @@ -65,11 +66,28 @@ async def citedy_status():
"developer_url": CITEDY_DEVELOPER_URL,
"billing_url": CITEDY_BILLING_URL,
}
except HTTPError as e:
logger.warning("Citedy status rejected API key: HTTP %s", e.code)
reconnect_required = e.code in (401, 403)
return {
"configured": True,
"api_key_prefix": api_key[:20] + "..." if len(api_key) > 20 else api_key,
"status": "invalid" if reconnect_required else "unavailable",
"balance": None,
"error": (
"Citedy key needs reconnect"
if reconnect_required
else "Could not connect to Citedy API"
),
"developer_url": CITEDY_DEVELOPER_URL,
"billing_url": CITEDY_BILLING_URL,
}
except URLError as e:
logger.warning("Failed to fetch Citedy status: %s", e)
return {
"configured": True,
"api_key_prefix": api_key[:20] + "..." if len(api_key) > 20 else api_key,
"status": "unavailable",
"balance": None,
"error": "Could not connect to Citedy API",
"developer_url": CITEDY_DEVELOPER_URL,
Expand All @@ -80,6 +98,7 @@ async def citedy_status():
return {
"configured": True,
"api_key_prefix": api_key[:20] + "..." if len(api_key) > 20 else api_key,
"status": "unavailable",
"balance": None,
"error": f"Could not check Citedy status ({type(e).__name__})",
"developer_url": CITEDY_DEVELOPER_URL,
Expand Down Expand Up @@ -109,7 +128,7 @@ async def save_citedy_api_key(body: dict):
config = load_config()
config.mcp.clients["citedy"] = MCPClientConfig(
name="citedy_mcp",
description="Citedy SEO & Marketing Tools (59 tools)",
description=CITEDY_MCP_DESCRIPTION,
enabled=True,
transport="streamable_http",
url="https://mcp.citedy.com/mcp",
Expand Down
16 changes: 15 additions & 1 deletion src/adclaw/app/routers/skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,23 @@ class HubInstallRequest(BaseModel):
router = APIRouter(prefix="/skills", tags=["skills"])


def _effective_skills(skills: list[SkillInfo]) -> list[SkillInfo]:
"""Return customer-visible skills, preferring customized overrides."""
source_rank = {"builtin": 0, "customized": 1, "active": 2}
effective: dict[str, SkillInfo] = {}
for skill in skills:
current = effective.get(skill.name)
if current is None or source_rank.get(skill.source, 0) >= source_rank.get(
current.source,
0,
):
effective[skill.name] = skill
return list(effective.values())


@router.get("")
async def list_skills() -> list[SkillSpec]:
all_skills = SkillService.list_all_skills()
all_skills = _effective_skills(SkillService.list_all_skills())

available_skills = list_available_skills()
skills_spec = []
Expand Down
2 changes: 1 addition & 1 deletion src/adclaw/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ class MCPConfig(BaseModel):
),
"citedy": MCPClientConfig(
name="citedy_mcp",
description="Citedy SEO & Marketing Tools (59 tools)",
description="Citedy SEO & Marketing Tools (70+ tools)",
enabled=bool(os.getenv("CITEDY_API_KEY")),
transport="streamable_http",
url="https://mcp.citedy.com/mcp",
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading