95cc9e2d28
mergeWithClaudeCodeAgents deduplicated by raw agent.name.toLowerCase() while matchesRequestedAgent strips invisible characters, the numeric sort prefix, and wrapper characters via stripAgentListSortPrefix. A project or user agent named with a zero-width prefix, quote wrappers, or a sort prefix survived as a visible duplicate of the hidden native build or demoted plan agent and matched subagent_type="build" or "plan", which let an OMO orchestrator reach the hidden execution agent the previous filter was meant to block. Apply the same canonicalization to the dedup key so visible aliases of hidden server agents collapse onto the hidden entry instead of bypassing the filter. Adds three regression tests covering ZWSP, quote-wrapper, and sort-prefix bypass paths. bun.lock: refresh platform optionalDependencies to 4.1.1 so frozen-lockfile install succeeds in CI.
88 lines
3.1 KiB
TypeScript
88 lines
3.1 KiB
TypeScript
import { getAgentConfigKey, getAgentDisplayName, stripAgentListSortPrefix } from "../../shared/agent-display-names"
|
|
import { loadUserAgents, loadProjectAgents } from "../../features/claude-code-agent-loader"
|
|
|
|
export type AgentMode = "subagent" | "primary" | "all" | undefined
|
|
|
|
export type AgentInfo = {
|
|
name: string
|
|
mode?: "subagent" | "primary" | "all"
|
|
hidden?: boolean
|
|
model?: string | { providerID: string; modelID: string }
|
|
}
|
|
|
|
export function sanitizeSubagentType(subagentType: string): string {
|
|
return subagentType.trim().replace(/^[\\\/"']+|[\\\/"']+$/g, "").trim()
|
|
}
|
|
|
|
export function mergeWithClaudeCodeAgents(
|
|
serverAgents: AgentInfo[],
|
|
directory: string | undefined,
|
|
): AgentInfo[] {
|
|
const userAgentsRecord = loadUserAgents()
|
|
const projectAgentsRecord = loadProjectAgents(directory)
|
|
|
|
const toAgentInfoList = (record: Record<string, { mode?: string; hidden?: boolean; model?: AgentInfo["model"] }>): AgentInfo[] =>
|
|
Object.entries(record).map(([name, config]) => ({
|
|
name,
|
|
mode: config.mode as AgentInfo["mode"],
|
|
hidden: config.hidden,
|
|
model: config.model,
|
|
}))
|
|
|
|
const mergedAgentMap = new Map<string, AgentInfo>()
|
|
const addIfAbsent = (agent: AgentInfo): void => {
|
|
const key = stripAgentListSortPrefix(agent.name).trim().toLowerCase()
|
|
if (!mergedAgentMap.has(key)) {
|
|
mergedAgentMap.set(key, agent)
|
|
}
|
|
}
|
|
|
|
for (const agent of serverAgents) addIfAbsent(agent)
|
|
for (const agent of toAgentInfoList(projectAgentsRecord)) addIfAbsent(agent)
|
|
for (const agent of toAgentInfoList(userAgentsRecord)) addIfAbsent(agent)
|
|
|
|
return Array.from(mergedAgentMap.values())
|
|
}
|
|
|
|
function buildComparableNames(agentName: string): Set<string> {
|
|
return new Set([
|
|
agentName,
|
|
getAgentDisplayName(agentName),
|
|
getAgentConfigKey(agentName),
|
|
].map(name => stripAgentListSortPrefix(name).trim().toLowerCase()))
|
|
}
|
|
|
|
function matchesRequestedAgent(agent: AgentInfo, requestedAgentName: string): boolean {
|
|
const comparableNames = buildComparableNames(requestedAgentName)
|
|
const listedAgentName = stripAgentListSortPrefix(agent.name).trim().toLowerCase()
|
|
const listedAgentConfigKey = getAgentConfigKey(agent.name).trim().toLowerCase()
|
|
|
|
return comparableNames.has(listedAgentName) || comparableNames.has(listedAgentConfigKey)
|
|
}
|
|
|
|
export function isTaskCallableAgentMode(mode: AgentMode): boolean {
|
|
return mode === "all" || mode === "subagent"
|
|
}
|
|
|
|
export function findPrimaryAgentMatch(
|
|
agents: AgentInfo[],
|
|
requestedAgentName: string,
|
|
): AgentInfo | undefined {
|
|
return agents.find(agent => agent.mode === "primary" && matchesRequestedAgent(agent, requestedAgentName))
|
|
}
|
|
|
|
export function findCallableAgentMatch(
|
|
agents: AgentInfo[],
|
|
requestedAgentName: string,
|
|
): AgentInfo | undefined {
|
|
return agents.find(agent => isTaskCallableAgentMode(agent.mode) && agent.hidden !== true && matchesRequestedAgent(agent, requestedAgentName))
|
|
}
|
|
|
|
export function listCallableAgentNames(agents: AgentInfo[]): string {
|
|
return agents
|
|
.filter(agent => isTaskCallableAgentMode(agent.mode) && agent.hidden !== true)
|
|
.map(agent => stripAgentListSortPrefix(agent.name))
|
|
.sort()
|
|
.join(", ")
|
|
}
|