supermemory/apps/web/lib/plugin-space.ts
ved015 6f3c835e8f feat(web): add Cursor to Agents (#1361)
Adds Cursor projects to Agents spaces with source filters, legacy labels, icons, and Codex-style structured conversation rendering.

Tests: targeted Agents and plugin-document tests.
2026-07-25 22:09:28 +00:00

249 lines
6 KiB
TypeScript

import { normalizePluginClientId } from "@/lib/plugin-catalog"
import { isAgentContainerTag } from "@/lib/agent-space"
export type PluginSpaceInfo = {
pluginId:
| "agents"
| "claude-code"
| "openclaw"
| "opencode"
| "codex"
| "cursor"
| "amp"
| "hermes"
label: string
iconSrc: string | null
projectId?: string
}
type PluginDef = {
id: PluginSpaceInfo["pluginId"]
label: string
iconSrc: string | null
prefixes: string[]
}
const PLUGINS: PluginDef[] = [
{
id: "agents",
label: "Agents",
iconSrc: null,
prefixes: [
"user_project",
"repo",
"claudecode",
"codex",
"opencode",
"cursor",
],
},
{
id: "openclaw",
label: "OpenClaw",
iconSrc: "/images/plugins/openclaw.svg",
prefixes: ["openclaw"],
},
{
id: "opencode",
label: "OpenCode",
iconSrc: "/images/plugins/opencode.svg",
prefixes: ["opencode"],
},
{
id: "amp",
label: "Amp",
iconSrc: null,
prefixes: ["amp"],
},
{
id: "hermes",
label: "Hermes",
iconSrc: "/images/plugins/hermes.svg",
prefixes: ["hermes"],
},
]
function parsePluginRest(rest: string): { projectId?: string } {
if (!rest || rest === "default" || rest === "global") {
return { projectId: "Global" }
}
const userMatch = rest.match(/^user[_-]([0-9a-f]{6,64})$/i)
if (userMatch?.[1]) return { projectId: `User · ${userMatch[1].slice(0, 6)}` }
const projectMatch = rest.match(/^project[_-]([0-9a-f]{6,64})$/i)
if (projectMatch?.[1]) return { projectId: projectMatch[1].slice(0, 6) }
const hexOnly = rest.match(/^([0-9a-f]{6,64})$/i)
if (hexOnly?.[1]) return { projectId: hexOnly[1].slice(0, 6) }
return { projectId: rest.slice(0, 24) }
}
const PLUGIN_ICON_BY_LABEL: Record<string, string> = {
"Claude Code": "/images/plugins/claude-code.svg",
OpenClaw: "/images/plugins/openclaw.svg",
OpenCode: "/images/plugins/opencode.svg",
Codex: "/images/plugins/codex.png",
Cursor: "/images/plugins/cursor.png",
Hermes: "/images/plugins/hermes.svg",
}
export function pluginIconByLabel(
label: string | null | undefined,
): string | null {
if (!label) return null
return PLUGIN_ICON_BY_LABEL[label] ?? null
}
export function pluginInitial(label: string | null | undefined): string {
if (!label) return "?"
return label.trim().charAt(0).toUpperCase() || "?"
}
export type PluginDocSource = {
pluginId: "claude-code"
label: string
iconSrc: string
projectName?: string
formatLabel: string
type: "session_turn" | "project-knowledge" | "manual" | "unknown"
}
function formatLabelForType(t: string | undefined): {
formatLabel: string
type: PluginDocSource["type"]
} {
switch (t) {
case "session_turn":
return { formatLabel: "Session", type: "session_turn" }
case "project-knowledge":
return { formatLabel: "Project knowledge", type: "project-knowledge" }
case "manual":
return { formatLabel: "Note", type: "manual" }
default:
return { formatLabel: "Note", type: "unknown" }
}
}
export function detectPluginSource(
metadata: Record<string, unknown> | null | undefined,
documentSource?: string | null,
): PluginDocSource | null {
const sourceFromMeta =
metadata && typeof metadata.sm_source === "string"
? metadata.sm_source
: null
const source = (documentSource ?? sourceFromMeta)?.trim().toLowerCase()
if (source !== "claude-code-plugin" && source !== "claude-code") return null
const md = metadata ?? {}
const project =
typeof md.project === "string" && md.project.trim()
? md.project.trim()
: undefined
const t = typeof md.type === "string" ? md.type : undefined
const { formatLabel, type } = formatLabelForType(t)
return {
pluginId: "claude-code",
label: "Claude Code",
iconSrc: "/images/plugins/claude-code.svg",
projectName: project,
formatLabel,
type,
}
}
type DocumentSpaceCandidate = {
containerTags?: string[]
memoryEntries?: Array<{ spaceContainerTag?: string | null }> | null
}
function catalogIdForPluginSpace(
pluginId: PluginSpaceInfo["pluginId"],
): string {
return pluginId.replace(/-/g, "_")
}
/** Resolve the best container tag for a tool/plugin document. */
export function getToolDocumentSpace(
document: DocumentSpaceCandidate,
pluginClientId?: string | null,
): string | null {
const containerTags = document.containerTags ?? []
const memorySpaceTags = (document.memoryEntries ?? [])
.map((entry) => entry.spaceContainerTag)
.filter((tag): tag is string => !!tag)
const allTags = [...containerTags, ...memorySpaceTags]
if (pluginClientId) {
const normalizedClient = normalizePluginClientId(pluginClientId)
for (const tag of allTags) {
const pluginSpace = detectPluginSpace(tag)
if (
pluginSpace &&
catalogIdForPluginSpace(pluginSpace.pluginId) === normalizedClient
) {
return tag
}
}
}
for (const tag of allTags) {
if (detectPluginSpace(tag)) return tag
}
return allTags[0] ?? null
}
export function detectPluginSpace(
containerTag: string,
): PluginSpaceInfo | null {
if (!containerTag) return null
if (isAgentContainerTag(containerTag)) {
const agents = PLUGINS[0]
if (agents) {
const matchingPrefix = agents.prefixes.find(
(prefix) =>
containerTag === prefix ||
(containerTag.startsWith(prefix) &&
["_", "-"].includes(containerTag[prefix.length] ?? "")),
)
if (matchingPrefix) {
const rest = containerTag.slice(matchingPrefix.length + 1)
return {
pluginId: agents.id,
label: agents.label,
iconSrc: agents.iconSrc,
projectId: parsePluginRest(rest).projectId,
}
}
}
}
for (const plugin of PLUGINS.slice(1)) {
for (const prefix of plugin.prefixes) {
if (containerTag === prefix) {
return {
pluginId: plugin.id,
label: plugin.label,
iconSrc: plugin.iconSrc,
projectId: "Global",
}
}
const separator = containerTag[prefix.length]
if (
containerTag.startsWith(prefix) &&
(separator === "_" || separator === "-")
) {
const rest = containerTag.slice(prefix.length + 1)
const parsed = parsePluginRest(rest)
return {
pluginId: plugin.id,
label: plugin.label,
iconSrc: plugin.iconSrc,
projectId: parsed.projectId,
}
}
}
}
return null
}