From 8288ed978b859124a1eb4e4fd2d6767e212ac596 Mon Sep 17 00:00:00 2001 From: ved015 Date: Thu, 4 Jun 2026 18:52:15 +0530 Subject: [PATCH] Fix plugin memory parser attribution --- apps/web/lib/plugin-catalog.ts | 1 + apps/web/lib/plugin-document.ts | 237 ++++++++++++++++++++++++++------ apps/web/lib/plugin-space.ts | 13 +- 3 files changed, 207 insertions(+), 44 deletions(-) diff --git a/apps/web/lib/plugin-catalog.ts b/apps/web/lib/plugin-catalog.ts index f94a4e929..fb5113f45 100644 --- a/apps/web/lib/plugin-catalog.ts +++ b/apps/web/lib/plugin-catalog.ts @@ -142,6 +142,7 @@ const SPACE_TO_CATALOG_ID: Record = { codex: "codex", opencode: "opencode", openclaw: "openclaw", + hermes: "hermes", } export function spacePluginIdToCatalogId(spacePluginId: string): string | null { diff --git a/apps/web/lib/plugin-document.ts b/apps/web/lib/plugin-document.ts index c5f79db32..9ee34ef4e 100644 --- a/apps/web/lib/plugin-document.ts +++ b/apps/web/lib/plugin-document.ts @@ -1,6 +1,11 @@ import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" import type { z } from "zod" -import { detectPluginSource, pluginIconByLabel } from "@/lib/plugin-space" +import { + detectPluginSource, + detectPluginSpace, + pluginIconByLabel, + type PluginSpaceInfo, +} from "@/lib/plugin-space" type DocumentsResponse = z.infer type DocumentWithMemories = DocumentsResponse["documents"][0] @@ -8,10 +13,19 @@ type DocumentWithMemories = DocumentsResponse["documents"][0] export type PluginDocumentKind = | "codex-session" | "codex-save" + | "plugin-session" + | "plugin-save" | "amp-thread" | "openclaw-session" | "claude-code-doc" +type PluginIdentity = { + pluginId: PluginSpaceInfo["pluginId"] + label: string + iconSrc: string | null + projectId?: string +} + export interface PluginArtifact { label: string value: string @@ -76,11 +90,108 @@ function formatClientName(value: string | null | undefined): string | null { if (lower === "claude code") return "Claude Code" if (lower === "opencode") return "OpenCode" if (lower === "openclaw") return "OpenClaw" + if (lower === "hermes") return "Hermes" if (lower === "amp") return "Amp" return normalized.replace(/\b\w/g, (match) => match.toUpperCase()) } +function pluginIdentityFromSource( + value: string | null | undefined, +): PluginIdentity | null { + if (!value) return null + + const normalized = value.trim().toLowerCase().replace(/_/g, "-") + if (!normalized) return null + + switch (normalized) { + case "claude-code": + case "claude-code-plugin": + return { + pluginId: "claude-code", + label: "Claude Code", + iconSrc: "/images/plugins/claude-code.svg", + } + case "codex": + return { + pluginId: "codex", + label: "Codex", + iconSrc: "/images/plugins/codex.png", + } + case "openclaw": + return { + pluginId: "openclaw", + label: "OpenClaw", + iconSrc: "/images/plugins/openclaw.svg", + } + case "hermes": + return { + pluginId: "hermes", + label: "Hermes", + iconSrc: "/images/plugins/hermes.svg", + } + case "opencode": + return { + pluginId: "opencode", + label: "OpenCode", + iconSrc: "/images/plugins/opencode.svg", + } + case "amp": + return { + pluginId: "amp", + label: "Amp", + iconSrc: null, + } + default: + return null + } +} + +function pluginIdentityFromSpace( + document: DocumentWithMemories, +): PluginIdentity | null { + const documentWithTags = document as DocumentWithMemories & { + containerTags?: unknown + } + const containerTags = Array.isArray(documentWithTags.containerTags) + ? documentWithTags.containerTags.filter( + (tag): tag is string => typeof tag === "string" && !!tag, + ) + : [] + const memorySpaceTags = Array.isArray(document.memoryEntries) + ? document.memoryEntries + .map((entry) => entry?.spaceContainerTag) + .filter((tag): tag is string => typeof tag === "string" && !!tag) + : [] + + for (const tag of [...containerTags, ...memorySpaceTags]) { + const plugin = detectPluginSpace(tag) + if (plugin) return plugin + } + + return null +} + +function getDocumentPluginIdentity( + document: DocumentWithMemories, + metadata: Record, +): PluginIdentity | null { + const sourceCandidates = [ + typeof document.source === "string" ? document.source : null, + typeof metadata.sm_source === "string" ? metadata.sm_source : null, + typeof metadata.sm_internal_mcp_client_name === "string" + ? metadata.sm_internal_mcp_client_name + : null, + ] + + for (const source of sourceCandidates) { + const plugin = pluginIdentityFromSource(source) + if (plugin) return plugin + } + + return pluginIdentityFromSpace(document) +} + function extractArtifacts(text: string): { cleanText: string artifacts: PluginArtifact[] @@ -180,7 +291,12 @@ function takePreview(text: string, maxLength = 180): string { return `${normalized.slice(0, maxLength - 1).trimEnd()}...` } -function parseSaveSections(content: string): ParsedPluginDocument | null { +function parseSaveSections( + content: string, + plugin: PluginIdentity | null, +): ParsedPluginDocument | null { + if (!plugin) return null + const match = content.match(/\[SAVE:([^\]]+)\]([\s\S]*?)\[\/SAVE\]/i) if (!match) return null @@ -238,12 +354,13 @@ function parseSaveSections(content: string): ParsedPluginDocument | null { "Saved project note" return { - kind: "codex-save", - pluginLabel: "Codex", + kind: plugin.pluginId === "codex" ? "codex-save" : "plugin-save", + pluginLabel: plugin.label, + pluginIconSrc: plugin.iconSrc ?? undefined, formatLabel: "Saved note", title: "Saved memory note", preview: takePreview( - sections[0]?.value ?? "Saved project knowledge from Codex", + sections[0]?.value ?? `Saved project knowledge from ${plugin.label}`, 140, ), summary: takePreview(summary, 220), @@ -259,9 +376,10 @@ function parseSaveSections(content: string): ParsedPluginDocument | null { function parseSessionTranscript( content: string, config: { - kind: "codex-session" | "amp-thread" + kind: "codex-session" | "amp-thread" | "plugin-session" headerLabel: "Session" | "Amp thread" pluginLabel: string + pluginIconSrc?: string | null formatLabel: string }, ): ParsedPluginDocument | null { @@ -289,6 +407,7 @@ function parseSessionTranscript( return { kind: config.kind, pluginLabel: config.pluginLabel, + pluginIconSrc: config.pluginIconSrc ?? undefined, formatLabel: config.formatLabel, title: `${config.pluginLabel} conversation`, preview: takePreview(previewSource, 140), @@ -302,7 +421,17 @@ function parseSessionTranscript( } } -function parseOpenClawTranscript(content: string): ParsedPluginDocument | null { +function parseRoleBlockTranscript( + content: string, + plugin: PluginIdentity | null, +): ParsedPluginDocument | null { + if ( + !plugin || + (plugin.pluginId !== "openclaw" && plugin.pluginId !== "hermes") + ) { + return null + } + const { messages, artifacts } = parseRoleBlockMessages(content) if (messages.length === 0) return null @@ -312,12 +441,14 @@ function parseOpenClawTranscript(content: string): ParsedPluginDocument | null { "Conversation" return { - kind: "openclaw-session", - pluginLabel: "OpenClaw", + kind: + plugin.pluginId === "openclaw" ? "openclaw-session" : "plugin-session", + pluginLabel: plugin.label, + pluginIconSrc: plugin.iconSrc ?? undefined, formatLabel: "Conversation", - title: "OpenClaw conversation", + title: `${plugin.label} conversation`, preview: takePreview(previewSource, 140), - summary: `${messages.length} message${messages.length === 1 ? "" : "s"} captured from OpenClaw.`, + summary: `${messages.length} message${messages.length === 1 ? "" : "s"} captured from ${plugin.label}.`, artifacts, messages, sections: [], @@ -417,7 +548,8 @@ export function claudeCodeTokenBadge( document: DocumentWithMemories, ): string | null { const meta = (document.metadata ?? {}) as Record - if (getDocumentPluginSource(document, meta) !== "claude-code-plugin") { + const plugin = getDocumentPluginIdentity(document, meta) + if (plugin?.pluginId !== "claude-code") { return null } const tokens = document.tokenCount @@ -431,10 +563,26 @@ function parseClaudeCodeByMetadata( ): ParsedPluginDocument | null { const docSource = getDocumentPluginSource(document, metadata) let source = detectPluginSource(metadata, docSource) + const plugin = getDocumentPluginIdentity(document, metadata) const rawContent = typeof document.content === "string" ? document.content : "" + if (!source && plugin?.pluginId === "claude-code") { + const project = + typeof metadata.project === "string" && metadata.project.trim() + ? metadata.project.trim() + : plugin.projectId + source = { + pluginId: "claude-code", + label: "Claude Code", + iconSrc: "/images/plugins/claude-code.svg", + projectName: project, + formatLabel: "Session", + type: "session_turn", + } + } + if (!source) { if (CLAUDE_CODE_CONTENT_RE.test(rawContent)) { const md = metadata ?? {} @@ -488,10 +636,7 @@ export function parsePluginDocument( if (!document) return null const metadata = (document.metadata ?? {}) as Record - - if (getDocumentPluginSource(document, metadata) === "claude-code-plugin") { - return withIcon(parseClaudeCodeByMetadata(document, metadata)) - } + const plugin = getDocumentPluginIdentity(document, metadata) const content = normalizeContent( typeof document.content === "string" ? document.content : "", @@ -504,39 +649,49 @@ export function parsePluginDocument( ) if (content) { - const codexSave = parseSaveSections(content) - if (codexSave) { + const saveDoc = parseSaveSections(content, plugin) + if (saveDoc) { if (clientName) { - codexSave.clientLabel = "Client" - codexSave.clientValue = clientName + saveDoc.clientLabel = "Client" + saveDoc.clientValue = clientName } - return withIcon(codexSave) + return withIcon(saveDoc) } - const codexSession = parseSessionTranscript(content, { - kind: "codex-session", - headerLabel: "Session", - pluginLabel: "Codex", - formatLabel: "Conversation", - }) - if (codexSession) { - if (clientName) { - codexSession.clientLabel = "Client" - codexSession.clientValue = clientName + if (plugin?.pluginId === "claude-code") { + return withIcon(parseClaudeCodeByMetadata(document, metadata)) + } + + if (plugin?.pluginId === "codex") { + const codexSession = parseSessionTranscript(content, { + kind: "codex-session", + headerLabel: "Session", + pluginLabel: plugin.label, + pluginIconSrc: plugin.iconSrc, + formatLabel: "Conversation", + }) + if (codexSession) { + if (clientName) { + codexSession.clientLabel = "Client" + codexSession.clientValue = clientName + } + return withIcon(codexSession) } - return withIcon(codexSession) } - const ampThread = parseSessionTranscript(content, { - kind: "amp-thread", - headerLabel: "Amp thread", - pluginLabel: "Amp", - formatLabel: "Conversation", - }) - if (ampThread) return withIcon(ampThread) + if (plugin?.pluginId === "amp") { + const ampThread = parseSessionTranscript(content, { + kind: "amp-thread", + headerLabel: "Amp thread", + pluginLabel: plugin.label, + pluginIconSrc: plugin.iconSrc, + formatLabel: "Conversation", + }) + if (ampThread) return withIcon(ampThread) + } - const openClawSession = parseOpenClawTranscript(content) - if (openClawSession) return withIcon(openClawSession) + const roleBlockSession = parseRoleBlockTranscript(content, plugin) + if (roleBlockSession) return withIcon(roleBlockSession) } return withIcon(parseClaudeCodeByMetadata(document, metadata)) diff --git a/apps/web/lib/plugin-space.ts b/apps/web/lib/plugin-space.ts index b42a2fdc9..a4e4d7df2 100644 --- a/apps/web/lib/plugin-space.ts +++ b/apps/web/lib/plugin-space.ts @@ -1,7 +1,7 @@ import { normalizePluginClientId } from "@/lib/plugin-catalog" export type PluginSpaceInfo = { - pluginId: "claude-code" | "openclaw" | "opencode" | "codex" | "amp" + pluginId: "claude-code" | "openclaw" | "opencode" | "codex" | "amp" | "hermes" label: string iconSrc: string | null projectId?: string @@ -45,6 +45,12 @@ const PLUGINS: PluginDef[] = [ iconSrc: null, prefixes: ["amp"], }, + { + id: "hermes", + label: "Hermes", + iconSrc: "/images/plugins/hermes.svg", + prefixes: ["hermes"], + }, ] function parsePluginRest(rest: string): { projectId?: string } { @@ -65,6 +71,7 @@ const PLUGIN_ICON_BY_LABEL: Record = { OpenClaw: "/images/plugins/openclaw.svg", OpenCode: "/images/plugins/opencode.svg", Codex: "/images/plugins/codex.png", + Hermes: "/images/plugins/hermes.svg", } export function pluginIconByLabel( @@ -112,8 +119,8 @@ export function detectPluginSource( metadata && typeof metadata.sm_source === "string" ? metadata.sm_source : null - const source = documentSource ?? sourceFromMeta - if (source !== "claude-code-plugin") return null + const source = (documentSource ?? sourceFromMeta)?.trim().toLowerCase() + if (source !== "claude-code-plugin" && source !== "claude-code") return null const md = metadata ?? {} const project =