| import { feature } from 'bun:bundle' |
| import type { |
| ContentBlockParam, |
| TextBlockParam, |
| } from '@anthropic-ai/sdk/resources' |
| import { randomUUID } from 'crypto' |
| import { setPromptId } from 'src/bootstrap/state.js' |
| import { |
| builtInCommandNames, |
| type Command, |
| type CommandBase, |
| findCommand, |
| getCommand, |
| getCommandName, |
| hasCommand, |
| type PromptCommand, |
| } from 'src/commands.js' |
| import { NO_CONTENT_MESSAGE } from 'src/constants/messages.js' |
| import type { SetToolJSXFn, ToolUseContext } from 'src/Tool.js' |
| import type { |
| AssistantMessage, |
| AttachmentMessage, |
| Message, |
| NormalizedUserMessage, |
| ProgressMessage, |
| UserMessage, |
| } from 'src/types/message.js' |
| import { addInvokedSkill, getSessionId } from '../../bootstrap/state.js' |
| import { COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG } from '../../constants/xml.js' |
| import type { CanUseToolFn } from '../../hooks/useCanUseTool.js' |
| import { |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, |
| logEvent, |
| } from '../../services/analytics/index.js' |
| import { getDumpPromptsPath } from '../../services/api/dumpPrompts.js' |
| import { buildPostCompactMessages } from '../../services/compact/compact.js' |
| import { resetMicrocompactState } from '../../services/compact/microCompact.js' |
| import type { Progress as AgentProgress } from '../../tools/AgentTool/AgentTool.js' |
| import { runAgent } from '../../tools/AgentTool/runAgent.js' |
| import { renderToolUseProgressMessage } from '../../tools/AgentTool/UI.js' |
| import type { CommandResultDisplay } from '../../types/command.js' |
| import { createAbortController } from '../abortController.js' |
| import { getAgentContext } from '../agentContext.js' |
| import { |
| createAttachmentMessage, |
| getAttachmentMessages, |
| } from '../attachments.js' |
| import { logForDebugging } from '../debug.js' |
| import { isEnvTruthy } from '../envUtils.js' |
| import { AbortError, MalformedCommandError } from '../errors.js' |
| import { getDisplayPath } from '../file.js' |
| import { |
| extractResultText, |
| prepareForkedCommandContext, |
| } from '../forkedAgent.js' |
| import { getFsImplementation } from '../fsOperations.js' |
| import { isFullscreenEnvEnabled } from '../fullscreen.js' |
| import { toArray } from '../generators.js' |
| import { registerSkillHooks } from '../hooks/registerSkillHooks.js' |
| import { logError } from '../log.js' |
| import { enqueuePendingNotification } from '../messageQueueManager.js' |
| import { |
| createCommandInputMessage, |
| createSyntheticUserCaveatMessage, |
| createSystemMessage, |
| createUserInterruptionMessage, |
| createUserMessage, |
| formatCommandInputTags, |
| isCompactBoundaryMessage, |
| isSystemLocalCommandMessage, |
| normalizeMessages, |
| prepareUserContent, |
| } from '../messages.js' |
| import type { ModelAlias } from '../model/aliases.js' |
| import { parseToolListFromCLI } from '../permissions/permissionSetup.js' |
| import { hasPermissionsToUseTool } from '../permissions/permissions.js' |
| import { |
| isOfficialMarketplaceName, |
| parsePluginIdentifier, |
| } from '../plugins/pluginIdentifier.js' |
| import { |
| isRestrictedToPluginOnly, |
| isSourceAdminTrusted, |
| } from '../settings/pluginOnlyPolicy.js' |
| import { parseSlashCommand } from '../slashCommandParsing.js' |
| import { sleep } from '../sleep.js' |
| import { recordSkillUsage } from '../suggestions/skillUsageTracking.js' |
| import { logOTelEvent, redactIfDisabled } from '../telemetry/events.js' |
| import { buildPluginCommandTelemetryFields } from '../telemetry/pluginTelemetry.js' |
| import { getAssistantMessageContentLength } from '../tokens.js' |
| import { createAgentId } from '../uuid.js' |
| import { getWorkload } from '../workloadContext.js' |
| import type { |
| ProcessUserInputBaseResult, |
| ProcessUserInputContext, |
| } from './processUserInput.js' |
|
|
| type SlashCommandResult = ProcessUserInputBaseResult & { |
| command: Command |
| } |
|
|
| |
| |
| |
| const MCP_SETTLE_POLL_MS = 200 |
| const MCP_SETTLE_TIMEOUT_MS = 10_000 |
|
|
| |
| |
| |
| async function executeForkedSlashCommand( |
| command: CommandBase & PromptCommand, |
| args: string, |
| context: ProcessUserInputContext, |
| precedingInputBlocks: ContentBlockParam[], |
| setToolJSX: SetToolJSXFn, |
| canUseTool: CanUseToolFn, |
| ): Promise<SlashCommandResult> { |
| const agentId = createAgentId() |
|
|
| const pluginMarketplace = command.pluginInfo |
| ? parsePluginIdentifier(command.pluginInfo.repository).marketplace |
| : undefined |
| logEvent('tengu_slash_command_forked', { |
| command_name: |
| command.name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| invocation_trigger: |
| 'user-slash' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| ...(command.pluginInfo && { |
| _PROTO_plugin_name: command.pluginInfo.pluginManifest |
| .name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, |
| ...(pluginMarketplace && { |
| _PROTO_marketplace_name: |
| pluginMarketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, |
| }), |
| ...buildPluginCommandTelemetryFields(command.pluginInfo), |
| }), |
| }) |
|
|
| const { skillContent, modifiedGetAppState, baseAgent, promptMessages } = |
| await prepareForkedCommandContext(command, args, context) |
|
|
| |
| const agentDefinition = |
| command.effort !== undefined |
| ? { ...baseAgent, effort: command.effort } |
| : baseAgent |
|
|
| logForDebugging( |
| `Executing forked slash command /${command.name} with agent ${agentDefinition.agentType}`, |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if (feature('KAIROS') && (await context.getAppState()).kairosEnabled) { |
| |
| |
| |
| const bgAbortController = createAbortController() |
| const commandName = getCommandName(command) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const spawnTimeWorkload = getWorkload() |
|
|
| |
| |
| |
| |
| |
| |
| const enqueueResult = (value: string): void => |
| enqueuePendingNotification({ |
| value, |
| mode: 'prompt', |
| priority: 'later', |
| isMeta: true, |
| skipSlashCommands: true, |
| workload: spawnTimeWorkload, |
| }) |
|
|
| void (async () => { |
| |
| |
| |
| |
| |
| |
| const deadline = Date.now() + MCP_SETTLE_TIMEOUT_MS |
| while (Date.now() < deadline) { |
| const s = context.getAppState() |
| if (!s.mcp.clients.some(c => c.type === 'pending')) break |
| await sleep(MCP_SETTLE_POLL_MS) |
| } |
| const freshTools = |
| context.options.refreshTools?.() ?? context.options.tools |
|
|
| const agentMessages: Message[] = [] |
| for await (const message of runAgent({ |
| agentDefinition, |
| promptMessages, |
| toolUseContext: { |
| ...context, |
| getAppState: modifiedGetAppState, |
| abortController: bgAbortController, |
| }, |
| canUseTool, |
| isAsync: true, |
| querySource: 'agent:custom', |
| model: command.model as ModelAlias | undefined, |
| availableTools: freshTools, |
| override: { agentId }, |
| })) { |
| agentMessages.push(message) |
| } |
| const resultText = extractResultText(agentMessages, 'Command completed') |
| logForDebugging( |
| `Background forked command /${commandName} completed (agent ${agentId})`, |
| ) |
| enqueueResult( |
| `<scheduled-task-result command="/${commandName}">\n${resultText}\n</scheduled-task-result>`, |
| ) |
| })().catch(err => { |
| logError(err) |
| enqueueResult( |
| `<scheduled-task-result command="/${commandName}" status="failed">\n${err instanceof Error ? err.message : String(err)}\n</scheduled-task-result>`, |
| ) |
| }) |
|
|
| |
| |
| return { messages: [], shouldQuery: false, command } |
| } |
|
|
| |
| const agentMessages: Message[] = [] |
|
|
| |
| const progressMessages: ProgressMessage<AgentProgress>[] = [] |
| const parentToolUseID = `forked-command-${command.name}` |
| let toolUseCounter = 0 |
|
|
| |
| const createProgressMessage = ( |
| message: AssistantMessage | NormalizedUserMessage, |
| ): ProgressMessage<AgentProgress> => { |
| toolUseCounter++ |
| return { |
| type: 'progress', |
| data: { |
| message, |
| type: 'agent_progress', |
| prompt: skillContent, |
| agentId, |
| }, |
| parentToolUseID, |
| toolUseID: `${parentToolUseID}-${toolUseCounter}`, |
| timestamp: new Date().toISOString(), |
| uuid: randomUUID(), |
| } |
| } |
|
|
| |
| const updateProgress = (): void => { |
| setToolJSX({ |
| jsx: renderToolUseProgressMessage(progressMessages, { |
| tools: context.options.tools, |
| verbose: false, |
| }), |
| shouldHidePromptInput: false, |
| shouldContinueAnimation: true, |
| showSpinner: true, |
| }) |
| } |
|
|
| |
| updateProgress() |
|
|
| |
| try { |
| for await (const message of runAgent({ |
| agentDefinition, |
| promptMessages, |
| toolUseContext: { |
| ...context, |
| getAppState: modifiedGetAppState, |
| }, |
| canUseTool, |
| isAsync: false, |
| querySource: 'agent:custom', |
| model: command.model as ModelAlias | undefined, |
| availableTools: context.options.tools, |
| })) { |
| agentMessages.push(message) |
| const normalizedNew = normalizeMessages([message]) |
|
|
| |
| if (message.type === 'assistant') { |
| |
| const contentLength = getAssistantMessageContentLength(message) |
| if (contentLength > 0) { |
| context.setResponseLength(len => len + contentLength) |
| } |
|
|
| const normalizedMsg = normalizedNew[0] |
| if (normalizedMsg && normalizedMsg.type === 'assistant') { |
| progressMessages.push(createProgressMessage(message)) |
| updateProgress() |
| } |
| } |
|
|
| |
| if (message.type === 'user') { |
| const normalizedMsg = normalizedNew[0] |
| if (normalizedMsg && normalizedMsg.type === 'user') { |
| progressMessages.push(createProgressMessage(normalizedMsg)) |
| updateProgress() |
| } |
| } |
| } |
| } finally { |
| |
| setToolJSX(null) |
| } |
|
|
| let resultText = extractResultText(agentMessages, 'Command completed') |
|
|
| logForDebugging( |
| `Forked slash command /${command.name} completed with agent ${agentId}`, |
| ) |
|
|
| |
| if ("external" === 'ant') { |
| resultText = `[ANT-ONLY] API calls: ${getDisplayPath(getDumpPromptsPath(agentId))}\n${resultText}` |
| } |
|
|
| |
| const messages: UserMessage[] = [ |
| createUserMessage({ |
| content: prepareUserContent({ |
| inputString: `/${getCommandName(command)} ${args}`.trim(), |
| precedingInputBlocks, |
| }), |
| }), |
| createUserMessage({ |
| content: `<local-command-stdout>\n${resultText}\n</local-command-stdout>`, |
| }), |
| ] |
|
|
| return { |
| messages, |
| shouldQuery: false, |
| command, |
| resultText, |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function looksLikeCommand(commandName: string): boolean { |
| |
| |
| return !/[^a-zA-Z0-9:\-_]/.test(commandName) |
| } |
|
|
| export async function processSlashCommand( |
| inputString: string, |
| precedingInputBlocks: ContentBlockParam[], |
| imageContentBlocks: ContentBlockParam[], |
| attachmentMessages: AttachmentMessage[], |
| context: ProcessUserInputContext, |
| setToolJSX: SetToolJSXFn, |
| uuid?: string, |
| isAlreadyProcessing?: boolean, |
| canUseTool?: CanUseToolFn, |
| ): Promise<ProcessUserInputBaseResult> { |
| const parsed = parseSlashCommand(inputString) |
| if (!parsed) { |
| logEvent('tengu_input_slash_missing', {}) |
| const errorMessage = 'Commands are in the form `/command [args]`' |
| return { |
| messages: [ |
| createSyntheticUserCaveatMessage(), |
| ...attachmentMessages, |
| createUserMessage({ |
| content: prepareUserContent({ |
| inputString: errorMessage, |
| precedingInputBlocks, |
| }), |
| }), |
| ], |
| shouldQuery: false, |
| resultText: errorMessage, |
| } |
| } |
|
|
| const { commandName, args: parsedArgs, isMcp } = parsed |
|
|
| const sanitizedCommandName = isMcp |
| ? 'mcp' |
| : !builtInCommandNames().has(commandName) |
| ? 'custom' |
| : commandName |
|
|
| |
| if (!hasCommand(commandName, context.options.commands)) { |
| |
| |
| let isFilePath = false |
| try { |
| await getFsImplementation().stat(`/${commandName}`) |
| isFilePath = true |
| } catch { |
| |
| } |
| if (looksLikeCommand(commandName) && !isFilePath) { |
| logEvent('tengu_input_slash_invalid', { |
| input: |
| commandName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
|
|
| const unknownMessage = `Unknown skill: ${commandName}` |
| return { |
| messages: [ |
| createSyntheticUserCaveatMessage(), |
| ...attachmentMessages, |
| createUserMessage({ |
| content: prepareUserContent({ |
| inputString: unknownMessage, |
| precedingInputBlocks, |
| }), |
| }), |
| |
| |
| ...(parsedArgs |
| ? [ |
| createSystemMessage( |
| `Args from unknown skill: ${parsedArgs}`, |
| 'warning', |
| ), |
| ] |
| : []), |
| ], |
| shouldQuery: false, |
| resultText: unknownMessage, |
| } |
| } |
|
|
| const promptId = randomUUID() |
| setPromptId(promptId) |
| logEvent('tengu_input_prompt', {}) |
| |
| void logOTelEvent('user_prompt', { |
| prompt_length: String(inputString.length), |
| prompt: redactIfDisabled(inputString), |
| 'prompt.id': promptId, |
| }) |
| return { |
| messages: [ |
| createUserMessage({ |
| content: prepareUserContent({ inputString, precedingInputBlocks }), |
| uuid: uuid, |
| }), |
| ...attachmentMessages, |
| ], |
| shouldQuery: true, |
| } |
| } |
|
|
| |
|
|
| const { |
| messages: newMessages, |
| shouldQuery: messageShouldQuery, |
| allowedTools, |
| model, |
| effort, |
| command: returnedCommand, |
| resultText, |
| nextInput, |
| submitNextInput, |
| } = await getMessagesForSlashCommand( |
| commandName, |
| parsedArgs, |
| setToolJSX, |
| context, |
| precedingInputBlocks, |
| imageContentBlocks, |
| isAlreadyProcessing, |
| canUseTool, |
| uuid, |
| ) |
|
|
| |
| if (newMessages.length === 0) { |
| const eventData: Record<string, boolean | number | undefined> = { |
| input: |
| sanitizedCommandName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| } |
|
|
| |
| if (returnedCommand.type === 'prompt' && returnedCommand.pluginInfo) { |
| const { pluginManifest, repository } = returnedCommand.pluginInfo |
| const { marketplace } = parsePluginIdentifier(repository) |
| const isOfficial = isOfficialMarketplaceName(marketplace) |
| |
| |
| |
| eventData._PROTO_plugin_name = |
| pluginManifest.name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED |
| if (marketplace) { |
| eventData._PROTO_marketplace_name = |
| marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED |
| } |
| eventData.plugin_repository = ( |
| isOfficial ? repository : 'third-party' |
| ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS |
| eventData.plugin_name = ( |
| isOfficial ? pluginManifest.name : 'third-party' |
| ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS |
| if (isOfficial && pluginManifest.version) { |
| eventData.plugin_version = |
| pluginManifest.version as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS |
| } |
| Object.assign( |
| eventData, |
| buildPluginCommandTelemetryFields(returnedCommand.pluginInfo), |
| ) |
| } |
|
|
| logEvent('tengu_input_command', { |
| ...eventData, |
| invocation_trigger: |
| 'user-slash' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| ...("external" === 'ant' && { |
| skill_name: |
| commandName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| ...(returnedCommand.type === 'prompt' && { |
| skill_source: |
| returnedCommand.source as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(returnedCommand.loadedFrom && { |
| skill_loaded_from: |
| returnedCommand.loadedFrom as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(returnedCommand.kind && { |
| skill_kind: |
| returnedCommand.kind as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| }), |
| }) |
| return { |
| messages: [], |
| shouldQuery: false, |
|
|
| model, |
| nextInput, |
| submitNextInput, |
| } |
| } |
|
|
| |
| if ( |
| newMessages.length === 2 && |
| newMessages[1]!.type === 'user' && |
| typeof newMessages[1]!.message.content === 'string' && |
| newMessages[1]!.message.content.startsWith('Unknown command:') |
| ) { |
| |
| const looksLikeFilePath = |
| inputString.startsWith('/var') || |
| inputString.startsWith('/tmp') || |
| inputString.startsWith('/private') |
|
|
| if (!looksLikeFilePath) { |
| logEvent('tengu_input_slash_invalid', { |
| input: |
| commandName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| } |
|
|
| return { |
| messages: [createSyntheticUserCaveatMessage(), ...newMessages], |
| shouldQuery: messageShouldQuery, |
| allowedTools, |
|
|
| model, |
| } |
| } |
|
|
| |
| const eventData: Record<string, boolean | number | undefined> = { |
| input: |
| sanitizedCommandName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| } |
|
|
| |
| if (returnedCommand.type === 'prompt' && returnedCommand.pluginInfo) { |
| const { pluginManifest, repository } = returnedCommand.pluginInfo |
| const { marketplace } = parsePluginIdentifier(repository) |
| const isOfficial = isOfficialMarketplaceName(marketplace) |
| eventData._PROTO_plugin_name = |
| pluginManifest.name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED |
| if (marketplace) { |
| eventData._PROTO_marketplace_name = |
| marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED |
| } |
| eventData.plugin_repository = ( |
| isOfficial ? repository : 'third-party' |
| ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS |
| eventData.plugin_name = ( |
| isOfficial ? pluginManifest.name : 'third-party' |
| ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS |
| if (isOfficial && pluginManifest.version) { |
| eventData.plugin_version = |
| pluginManifest.version as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS |
| } |
| Object.assign( |
| eventData, |
| buildPluginCommandTelemetryFields(returnedCommand.pluginInfo), |
| ) |
| } |
|
|
| logEvent('tengu_input_command', { |
| ...eventData, |
| invocation_trigger: |
| 'user-slash' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| ...("external" === 'ant' && { |
| skill_name: |
| commandName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| ...(returnedCommand.type === 'prompt' && { |
| skill_source: |
| returnedCommand.source as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(returnedCommand.loadedFrom && { |
| skill_loaded_from: |
| returnedCommand.loadedFrom as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(returnedCommand.kind && { |
| skill_kind: |
| returnedCommand.kind as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| }), |
| }) |
|
|
| |
| const isCompactResult = |
| newMessages.length > 0 && |
| newMessages[0] && |
| isCompactBoundaryMessage(newMessages[0]) |
|
|
| return { |
| messages: |
| messageShouldQuery || |
| newMessages.every(isSystemLocalCommandMessage) || |
| isCompactResult |
| ? newMessages |
| : [createSyntheticUserCaveatMessage(), ...newMessages], |
| shouldQuery: messageShouldQuery, |
| allowedTools, |
| model, |
| effort, |
| resultText, |
| nextInput, |
| submitNextInput, |
| } |
| } |
|
|
| async function getMessagesForSlashCommand( |
| commandName: string, |
| args: string, |
| setToolJSX: SetToolJSXFn, |
| context: ProcessUserInputContext, |
| precedingInputBlocks: ContentBlockParam[], |
| imageContentBlocks: ContentBlockParam[], |
| _isAlreadyProcessing?: boolean, |
| canUseTool?: CanUseToolFn, |
| uuid?: string, |
| ): Promise<SlashCommandResult> { |
| const command = getCommand(commandName, context.options.commands) |
|
|
| |
| if (command.type === 'prompt' && command.userInvocable !== false) { |
| recordSkillUsage(commandName) |
| } |
|
|
| |
| |
| if (command.userInvocable === false) { |
| return { |
| messages: [ |
| createUserMessage({ |
| content: prepareUserContent({ |
| inputString: `/${commandName}`, |
| precedingInputBlocks, |
| }), |
| }), |
| createUserMessage({ |
| content: `This skill can only be invoked by Claude, not directly by users. Ask Claude to use the "${commandName}" skill for you.`, |
| }), |
| ], |
| shouldQuery: false, |
| command, |
| } |
| } |
|
|
| try { |
| switch (command.type) { |
| case 'local-jsx': { |
| return new Promise<SlashCommandResult>(resolve => { |
| let doneWasCalled = false |
| const onDone = ( |
| result?: string, |
| options?: { |
| display?: CommandResultDisplay |
| shouldQuery?: boolean |
| metaMessages?: string[] |
| nextInput?: string |
| submitNextInput?: boolean |
| }, |
| ) => { |
| doneWasCalled = true |
| |
| if (options?.display === 'skip') { |
| void resolve({ |
| messages: [], |
| shouldQuery: false, |
| command, |
| nextInput: options?.nextInput, |
| submitNextInput: options?.submitNextInput, |
| }) |
| return |
| } |
|
|
| |
| const metaMessages = (options?.metaMessages ?? []).map( |
| (content: string) => createUserMessage({ content, isMeta: true }), |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const skipTranscript = |
| isFullscreenEnvEnabled() && |
| typeof result === 'string' && |
| result.endsWith(' dismissed') |
|
|
| void resolve({ |
| messages: |
| options?.display === 'system' |
| ? skipTranscript |
| ? metaMessages |
| : [ |
| createCommandInputMessage( |
| formatCommandInput(command, args), |
| ), |
| createCommandInputMessage( |
| `<local-command-stdout>${result}</local-command-stdout>`, |
| ), |
| ...metaMessages, |
| ] |
| : [ |
| createUserMessage({ |
| content: prepareUserContent({ |
| inputString: formatCommandInput(command, args), |
| precedingInputBlocks, |
| }), |
| }), |
| result |
| ? createUserMessage({ |
| content: `<local-command-stdout>${result}</local-command-stdout>`, |
| }) |
| : createUserMessage({ |
| content: `<local-command-stdout>${NO_CONTENT_MESSAGE}</local-command-stdout>`, |
| }), |
| ...metaMessages, |
| ], |
| shouldQuery: options?.shouldQuery ?? false, |
| command, |
| nextInput: options?.nextInput, |
| submitNextInput: options?.submitNextInput, |
| }) |
| } |
|
|
| void command |
| .load() |
| .then(mod => mod.call(onDone, { ...context, canUseTool }, args)) |
| .then(jsx => { |
| if (jsx == null) return |
| if (context.options.isNonInteractiveSession) { |
| void resolve({ |
| messages: [], |
| shouldQuery: false, |
| command, |
| }) |
| return |
| } |
| |
| |
| |
| |
| |
| |
| |
| if (doneWasCalled) return |
| setToolJSX({ |
| jsx, |
| shouldHidePromptInput: true, |
| showSpinner: false, |
| isLocalJSXCommand: true, |
| isImmediate: command.immediate === true, |
| }) |
| }) |
| .catch(e => { |
| |
| |
| |
| logError(e) |
| if (doneWasCalled) return |
| doneWasCalled = true |
| setToolJSX({ |
| jsx: null, |
| shouldHidePromptInput: false, |
| clearLocalJSX: true, |
| }) |
| void resolve({ messages: [], shouldQuery: false, command }) |
| }) |
| }) |
| } |
| case 'local': { |
| const displayArgs = command.isSensitive && args.trim() ? '***' : args |
| const userMessage = createUserMessage({ |
| content: prepareUserContent({ |
| inputString: formatCommandInput(command, displayArgs), |
| precedingInputBlocks, |
| }), |
| }) |
|
|
| try { |
| const syntheticCaveatMessage = createSyntheticUserCaveatMessage() |
| const mod = await command.load() |
| const result = await mod.call(args, context) |
|
|
| if (result.type === 'skip') { |
| return { |
| messages: [], |
| shouldQuery: false, |
| command, |
| } |
| } |
|
|
| |
| if (result.type === 'compact') { |
| |
| |
| const slashCommandMessages = [ |
| syntheticCaveatMessage, |
| userMessage, |
| ...(result.displayText |
| ? [ |
| createUserMessage({ |
| content: `<local-command-stdout>${result.displayText}</local-command-stdout>`, |
| |
| |
| |
| |
| |
| timestamp: new Date(Date.now() + 100).toISOString(), |
| }), |
| ] |
| : []), |
| ] |
| const compactionResultWithSlashMessages = { |
| ...result.compactionResult, |
| messagesToKeep: [ |
| ...(result.compactionResult.messagesToKeep ?? []), |
| ...slashCommandMessages, |
| ], |
| } |
| |
| |
| |
| |
| resetMicrocompactState() |
| return { |
| messages: buildPostCompactMessages( |
| compactionResultWithSlashMessages, |
| ), |
| shouldQuery: false, |
| command, |
| } |
| } |
|
|
| |
| return { |
| messages: [ |
| userMessage, |
| createCommandInputMessage( |
| `<local-command-stdout>${result.value}</local-command-stdout>`, |
| ), |
| ], |
| shouldQuery: false, |
| command, |
| resultText: result.value, |
| } |
| } catch (e) { |
| logError(e) |
| return { |
| messages: [ |
| userMessage, |
| createCommandInputMessage( |
| `<local-command-stderr>${String(e)}</local-command-stderr>`, |
| ), |
| ], |
| shouldQuery: false, |
| command, |
| } |
| } |
| } |
| case 'prompt': { |
| try { |
| |
| if (command.context === 'fork') { |
| return await executeForkedSlashCommand( |
| command, |
| args, |
| context, |
| precedingInputBlocks, |
| setToolJSX, |
| canUseTool ?? hasPermissionsToUseTool, |
| ) |
| } |
|
|
| return await getMessagesForPromptSlashCommand( |
| command, |
| args, |
| context, |
| precedingInputBlocks, |
| imageContentBlocks, |
| uuid, |
| ) |
| } catch (e) { |
| |
| if (e instanceof AbortError) { |
| return { |
| messages: [ |
| createUserMessage({ |
| content: prepareUserContent({ |
| inputString: formatCommandInput(command, args), |
| precedingInputBlocks, |
| }), |
| }), |
| createUserInterruptionMessage({ toolUse: false }), |
| ], |
| shouldQuery: false, |
| command, |
| } |
| } |
| return { |
| messages: [ |
| createUserMessage({ |
| content: prepareUserContent({ |
| inputString: formatCommandInput(command, args), |
| precedingInputBlocks, |
| }), |
| }), |
| createUserMessage({ |
| content: `<local-command-stderr>${String(e)}</local-command-stderr>`, |
| }), |
| ], |
| shouldQuery: false, |
| command, |
| } |
| } |
| } |
| } |
| } catch (e) { |
| if (e instanceof MalformedCommandError) { |
| return { |
| messages: [ |
| createUserMessage({ |
| content: prepareUserContent({ |
| inputString: e.message, |
| precedingInputBlocks, |
| }), |
| }), |
| ], |
| shouldQuery: false, |
| command, |
| } |
| } |
| throw e |
| } |
| } |
|
|
| function formatCommandInput(command: CommandBase, args: string): string { |
| return formatCommandInputTags(getCommandName(command), args) |
| } |
|
|
| |
| |
| |
| |
| export function formatSkillLoadingMetadata( |
| skillName: string, |
| _progressMessage: string = 'loading', |
| ): string { |
| |
| return [ |
| `<${COMMAND_MESSAGE_TAG}>${skillName}</${COMMAND_MESSAGE_TAG}>`, |
| `<${COMMAND_NAME_TAG}>${skillName}</${COMMAND_NAME_TAG}>`, |
| `<skill-format>true</skill-format>`, |
| ].join('\n') |
| } |
|
|
| |
| |
| |
| function formatSlashCommandLoadingMetadata( |
| commandName: string, |
| args?: string, |
| ): string { |
| return [ |
| `<${COMMAND_MESSAGE_TAG}>${commandName}</${COMMAND_MESSAGE_TAG}>`, |
| `<${COMMAND_NAME_TAG}>/${commandName}</${COMMAND_NAME_TAG}>`, |
| args ? `<command-args>${args}</command-args>` : null, |
| ] |
| .filter(Boolean) |
| .join('\n') |
| } |
|
|
| |
| |
| |
| |
| |
| function formatCommandLoadingMetadata( |
| command: CommandBase & PromptCommand, |
| args?: string, |
| ): string { |
| |
| |
| |
| |
| if (command.userInvocable !== false) { |
| return formatSlashCommandLoadingMetadata(command.name, args) |
| } |
| |
| if ( |
| command.loadedFrom === 'skills' || |
| command.loadedFrom === 'plugin' || |
| command.loadedFrom === 'mcp' |
| ) { |
| return formatSkillLoadingMetadata(command.name, command.progressMessage) |
| } |
| return formatSlashCommandLoadingMetadata(command.name, args) |
| } |
|
|
| export async function processPromptSlashCommand( |
| commandName: string, |
| args: string, |
| commands: Command[], |
| context: ToolUseContext, |
| imageContentBlocks: ContentBlockParam[] = [], |
| ): Promise<SlashCommandResult> { |
| const command = findCommand(commandName, commands) |
| if (!command) { |
| throw new MalformedCommandError(`Unknown command: ${commandName}`) |
| } |
| if (command.type !== 'prompt') { |
| throw new Error( |
| `Unexpected ${command.type} command. Expected 'prompt' command. Use /${commandName} directly in the main conversation.`, |
| ) |
| } |
| return getMessagesForPromptSlashCommand( |
| command, |
| args, |
| context, |
| [], |
| imageContentBlocks, |
| ) |
| } |
|
|
| async function getMessagesForPromptSlashCommand( |
| command: CommandBase & PromptCommand, |
| args: string, |
| context: ToolUseContext, |
| precedingInputBlocks: ContentBlockParam[] = [], |
| imageContentBlocks: ContentBlockParam[] = [], |
| uuid?: string, |
| ): Promise<SlashCommandResult> { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if ( |
| feature('COORDINATOR_MODE') && |
| isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE) && |
| !context.agentId |
| ) { |
| const metadata = formatCommandLoadingMetadata(command, args) |
| const parts: string[] = [ |
| `Skill "/${command.name}" is available for workers.`, |
| ] |
| if (command.description) { |
| parts.push(`Description: ${command.description}`) |
| } |
| if (command.whenToUse) { |
| parts.push(`When to use: ${command.whenToUse}`) |
| } |
| const skillAllowedTools = command.allowedTools ?? [] |
| if (skillAllowedTools.length > 0) { |
| parts.push( |
| `This skill grants workers additional tool permissions: ${skillAllowedTools.join(', ')}`, |
| ) |
| } |
| parts.push( |
| `\nInstruct a worker to use this skill by including "Use the /${command.name} skill" in your Agent prompt. The worker has access to the Skill tool and will receive the skill's content and permissions when it invokes it.`, |
| ) |
| const summaryContent: ContentBlockParam[] = [ |
| { type: 'text', text: parts.join('\n') }, |
| ] |
| return { |
| messages: [ |
| createUserMessage({ content: metadata, uuid }), |
| createUserMessage({ content: summaryContent, isMeta: true }), |
| ], |
| shouldQuery: true, |
| model: command.model, |
| effort: command.effort, |
| command, |
| } |
| } |
|
|
| const result = await command.getPromptForCommand(args, context) |
|
|
| |
| |
| |
| const hooksAllowedForThisSkill = |
| !isRestrictedToPluginOnly('hooks') || isSourceAdminTrusted(command.source) |
| if (command.hooks && hooksAllowedForThisSkill) { |
| const sessionId = getSessionId() |
| registerSkillHooks( |
| context.setAppState, |
| sessionId, |
| command.hooks, |
| command.name, |
| command.type === 'prompt' ? command.skillRoot : undefined, |
| ) |
| } |
|
|
| |
| |
| |
| const skillPath = command.source |
| ? `${command.source}:${command.name}` |
| : command.name |
| const skillContent = result |
| .filter((b): b is TextBlockParam => b.type === 'text') |
| .map(b => b.text) |
| .join('\n\n') |
| addInvokedSkill( |
| command.name, |
| skillPath, |
| skillContent, |
| getAgentContext()?.agentId ?? null, |
| ) |
|
|
| const metadata = formatCommandLoadingMetadata(command, args) |
|
|
| const additionalAllowedTools = parseToolListFromCLI( |
| command.allowedTools ?? [], |
| ) |
|
|
| |
| const mainMessageContent: ContentBlockParam[] = |
| imageContentBlocks.length > 0 || precedingInputBlocks.length > 0 |
| ? [...imageContentBlocks, ...precedingInputBlocks, ...result] |
| : result |
|
|
| |
| |
| |
| |
| |
| const attachmentMessages = await toArray( |
| getAttachmentMessages( |
| result |
| .filter((block): block is TextBlockParam => block.type === 'text') |
| .map(block => block.text) |
| .join(' '), |
| context, |
| null, |
| [], |
| context.messages, |
| 'repl_main_thread', |
| { skipSkillDiscovery: true }, |
| ), |
| ) |
|
|
| const messages = [ |
| createUserMessage({ |
| content: metadata, |
| uuid, |
| }), |
| createUserMessage({ |
| content: mainMessageContent, |
| isMeta: true, |
| }), |
| ...attachmentMessages, |
| createAttachmentMessage({ |
| type: 'command_permissions', |
| allowedTools: additionalAllowedTools, |
| model: command.model, |
| }), |
| ] |
|
|
| return { |
| messages, |
| shouldQuery: true, |
| allowedTools: additionalAllowedTools, |
| model: command.model, |
| effort: command.effort, |
| command, |
| } |
| } |
|
|