f152569ed4
Add optional enabled_expansions field to keyword_detector config schema. When set, acts as an allowlist - only those expansion types fire. Empty array disables all expansions. Absent field keeps all enabled (backward-compatible). Also supports coexistence with disabled_keywords denylist for fine-grained control. - src/config/schema/keyword-detector.ts: add enabled_expansions field - src/hooks/keyword-detector/detector.ts: apply allowlist filter in detectKeywordsWithType - src/hooks/keyword-detector/hook.ts: pass enabled_expansions from config - src/hooks/keyword-detector/index.test.ts: add 4 tests for enabled_expansions behavior - assets/oh-my-opencode.schema.json: regenerate schema
212 lines
7.5 KiB
TypeScript
212 lines
7.5 KiB
TypeScript
import type { PluginInput } from "@opencode-ai/plugin"
|
|
import type { KeywordDetectorConfig } from "../../config/schema/keyword-detector"
|
|
import {
|
|
getMainSessionID,
|
|
getSessionAgent,
|
|
subagentSessions,
|
|
} from "../../features/claude-code-session-state"
|
|
import type { ContextCollector } from "../../features/context-injector"
|
|
import {
|
|
isRealUserTextPart,
|
|
isSyntheticOrInternalOnlyTextParts,
|
|
log,
|
|
} from "../../shared"
|
|
import {
|
|
isSystemDirective,
|
|
removeSystemReminders,
|
|
} from "../../shared/system-directive"
|
|
import type { RalphLoopHook } from "../ralph-loop"
|
|
import { isNonOmoAgent, isPlannerAgent } from "./constants"
|
|
import type { DetectedKeyword } from "./detector"
|
|
import { detectKeywordsWithType, extractPromptText, looksLikeSlashCommand } from "./detector"
|
|
|
|
function suppressComboStandalones(detected: DetectedKeyword[]): DetectedKeyword[] {
|
|
const hasCombo = detected.some((k) => k.type === "hyperplan-ultrawork")
|
|
if (!hasCombo) return detected
|
|
return detected.filter((k) => k.type !== "ultrawork" && k.type !== "hyperplan")
|
|
}
|
|
|
|
export function createKeywordDetectorHook(
|
|
ctx: PluginInput,
|
|
_collector?: ContextCollector,
|
|
_ralphLoop?: Pick<RalphLoopHook, "startLoop">,
|
|
config?: KeywordDetectorConfig,
|
|
) {
|
|
const disabledKeywords = config?.disabled_keywords
|
|
const enabledExpansions = config?.enabled_expansions
|
|
function getRuntimeVariant(input: { variant?: string }, message: Record<string, unknown>): string | undefined {
|
|
if (typeof message.variant === "string") {
|
|
return message.variant
|
|
}
|
|
|
|
return typeof input.variant === "string" ? input.variant : undefined
|
|
}
|
|
|
|
return {
|
|
"chat.message": async (
|
|
input: {
|
|
sessionID: string
|
|
agent?: string
|
|
model?: { providerID: string; modelID: string }
|
|
messageID?: string
|
|
variant?: string
|
|
},
|
|
output: {
|
|
message: Record<string, unknown>
|
|
parts: Array<{ type: string; text?: string; [key: string]: unknown }>
|
|
}
|
|
): Promise<void> => {
|
|
if (isSyntheticOrInternalOnlyTextParts(output.parts)) {
|
|
log(`[keyword-detector] Skipping synthetic/internal text message`, { sessionID: input.sessionID })
|
|
return
|
|
}
|
|
|
|
const promptText = extractPromptText(output.parts)
|
|
|
|
if (isSystemDirective(promptText)) {
|
|
log(`[keyword-detector] Skipping system directive message`, { sessionID: input.sessionID })
|
|
return
|
|
}
|
|
|
|
if (looksLikeSlashCommand(promptText)) {
|
|
log(`[keyword-detector] Skipping slash command invocation`, { sessionID: input.sessionID })
|
|
return
|
|
}
|
|
|
|
const currentAgent = getSessionAgent(input.sessionID) ?? input.agent
|
|
|
|
// Skip all keyword injection for non-OMO agents (e.g., OpenCode-Builder, Plan)
|
|
if (isNonOmoAgent(currentAgent)) {
|
|
log(`[keyword-detector] Skipping keyword injection for non-OMO agent`, { sessionID: input.sessionID, agent: currentAgent })
|
|
return
|
|
}
|
|
|
|
// Remove system-reminder content to prevent automated system messages from triggering mode keywords
|
|
const cleanText = removeSystemReminders(promptText)
|
|
const modelID = input.model?.modelID
|
|
let detectedKeywords = detectKeywordsWithType(cleanText, currentAgent, modelID, disabledKeywords, enabledExpansions)
|
|
detectedKeywords = suppressComboStandalones(detectedKeywords)
|
|
|
|
if (isPlannerAgent(currentAgent)) {
|
|
const preFilterCount = detectedKeywords.length
|
|
detectedKeywords = detectedKeywords.filter(
|
|
(k) => k.type !== "ultrawork" && k.type !== "hyperplan" && k.type !== "hyperplan-ultrawork"
|
|
)
|
|
if (preFilterCount > detectedKeywords.length) {
|
|
log(`[keyword-detector] Filtered ultrawork/hyperplan keywords for planner agent`, { sessionID: input.sessionID, agent: currentAgent })
|
|
}
|
|
}
|
|
|
|
if (detectedKeywords.length === 0) {
|
|
return
|
|
}
|
|
|
|
const isBackgroundTaskSession = subagentSessions.has(input.sessionID)
|
|
if (isBackgroundTaskSession) {
|
|
log(`[keyword-detector] Skipping keyword injection for background task session`, { sessionID: input.sessionID })
|
|
return
|
|
}
|
|
|
|
const mainSessionID = getMainSessionID()
|
|
const isNonMainSession = mainSessionID && input.sessionID !== mainSessionID
|
|
|
|
if (isNonMainSession) {
|
|
detectedKeywords = detectedKeywords.filter(
|
|
(k) => k.type === "ultrawork" || k.type === "hyperplan-ultrawork"
|
|
)
|
|
if (detectedKeywords.length === 0) {
|
|
log(`[keyword-detector] Skipping non-ultrawork keywords in non-main session`, {
|
|
sessionID: input.sessionID,
|
|
mainSessionID,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
const hasUltrawork = detectedKeywords.some((k) => k.type === "ultrawork")
|
|
if (hasUltrawork) {
|
|
const runtimeVariant = getRuntimeVariant(input, output.message)
|
|
const isRuntimeMax = runtimeVariant === "max"
|
|
|
|
log(`[keyword-detector] Ultrawork mode activated`, {
|
|
sessionID: input.sessionID,
|
|
runtimeVariant,
|
|
})
|
|
|
|
ctx.client.tui
|
|
.showToast({
|
|
body: {
|
|
title: "Ultrawork Mode Activated",
|
|
message: isRuntimeMax
|
|
? "Maximum precision engaged. All agents at your disposal."
|
|
: "Runtime variant preserved. All agents at your disposal.",
|
|
variant: "success" as const,
|
|
duration: 3000,
|
|
},
|
|
})
|
|
.catch((err) =>
|
|
log(`[keyword-detector] Failed to show toast`, {
|
|
error: err,
|
|
sessionID: input.sessionID,
|
|
})
|
|
)
|
|
|
|
}
|
|
|
|
const hasHyperplan = detectedKeywords.some((k) => k.type === "hyperplan")
|
|
if (hasHyperplan) {
|
|
log(`[keyword-detector] Hyperplan mode activated`, {
|
|
sessionID: input.sessionID,
|
|
})
|
|
|
|
ctx.client.tui
|
|
.showToast({
|
|
body: {
|
|
title: "Hyperplan Mode Activated",
|
|
message: "Adversarial planning engaged. 5 hostile members will cross-critique.",
|
|
variant: "success" as const,
|
|
duration: 3000,
|
|
},
|
|
})
|
|
.catch((err) =>
|
|
log(`[keyword-detector] Failed to show toast`, {
|
|
error: err,
|
|
sessionID: input.sessionID,
|
|
})
|
|
)
|
|
}
|
|
|
|
const hasHyperplanUltrawork = detectedKeywords.some((k) => k.type === "hyperplan-ultrawork")
|
|
if (hasHyperplanUltrawork) {
|
|
log(`[keyword-detector] Hyperplan Ultrawork mode activated`, { sessionID: input.sessionID })
|
|
ctx.client.tui
|
|
.showToast({
|
|
body: {
|
|
title: "Hyperplan Ultrawork Mode Activated",
|
|
message: "Ultrawork execution with adversarial hyperplan workflow.",
|
|
variant: "success" as const,
|
|
duration: 3000,
|
|
},
|
|
})
|
|
.catch((err) => log(`[keyword-detector] Failed to show toast`, { error: err, sessionID: input.sessionID }))
|
|
}
|
|
|
|
const textPartIndex = output.parts.findIndex(isRealUserTextPart)
|
|
if (textPartIndex === -1) {
|
|
log(`[keyword-detector] No text part found, skipping injection`, { sessionID: input.sessionID })
|
|
return
|
|
}
|
|
|
|
const allMessages = detectedKeywords.map((k) => k.message).join("\n\n")
|
|
const originalText = output.parts[textPartIndex].text ?? ""
|
|
|
|
output.parts[textPartIndex].text = `${allMessages}\n\n---\n\n${originalText}`
|
|
|
|
log(`[keyword-detector] Detected ${detectedKeywords.length} keywords`, {
|
|
sessionID: input.sessionID,
|
|
types: detectedKeywords.map((k) => k.type),
|
|
})
|
|
},
|
|
}
|
|
}
|