init claude-code
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import type { Tool, ToolUseContext } from '../Tool.js'
|
||||
import { BashTool } from '../tools/BashTool/BashTool.js'
|
||||
import { logForDebugging } from './debug.js'
|
||||
import { errorMessage, MalformedCommandError, ShellError } from './errors.js'
|
||||
import type { FrontmatterShell } from './frontmatterParser.js'
|
||||
import { createAssistantMessage } from './messages.js'
|
||||
import { hasPermissionsToUseTool } from './permissions/permissions.js'
|
||||
import { processToolResultBlock } from './toolResultStorage.js'
|
||||
|
||||
// Narrow structural slice both BashTool and PowerShellTool satisfy. We can't
|
||||
// use the base Tool type: it marks call()'s canUseTool/parentMessage as
|
||||
// required, but both concrete tools have them optional and the original code
|
||||
// called BashTool.call({ command }, ctx) with just 2 args. We can't use
|
||||
// `typeof BashTool` either: BashTool's input schema has fields (e.g.
|
||||
// _simulatedSedEdit) that PowerShellTool's does not.
|
||||
// NOTE: call() is invoked directly here, bypassing validateInput — any
|
||||
// load-bearing check must live in call() itself (see PR #23311).
|
||||
type ShellOut = { stdout: string; stderr: string; interrupted: boolean }
|
||||
type PromptShellTool = Tool & {
|
||||
call(
|
||||
input: { command: string },
|
||||
context: ToolUseContext,
|
||||
): Promise<{ data: ShellOut }>
|
||||
}
|
||||
|
||||
import { isPowerShellToolEnabled } from './shell/shellToolUtils.js'
|
||||
|
||||
// Lazy: this file is on the startup import chain (main → commands →
|
||||
// loadSkillsDir → here). A static import would load PowerShellTool.ts
|
||||
// (and transitively parser.ts, validators, etc.) at startup on all
|
||||
// platforms, defeating tools.ts's lazy require. Deferred until the
|
||||
// first skill with `shell: powershell` actually runs.
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const getPowerShellTool = (() => {
|
||||
let cached: PromptShellTool | undefined
|
||||
return (): PromptShellTool => {
|
||||
if (!cached) {
|
||||
cached = (
|
||||
require('../tools/PowerShellTool/PowerShellTool.js') as typeof import('../tools/PowerShellTool/PowerShellTool.js')
|
||||
).PowerShellTool
|
||||
}
|
||||
return cached
|
||||
}
|
||||
})()
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
// Pattern for code blocks: ```! command ```
|
||||
const BLOCK_PATTERN = /```!\s*\n?([\s\S]*?)\n?```/g
|
||||
|
||||
// Pattern for inline: !`command`
|
||||
// Uses a positive lookbehind to require whitespace or start-of-line before !
|
||||
// This prevents false matches inside markdown inline code spans like `!!` or
|
||||
// adjacent spans like `foo`!`bar`, and shell variables like $!
|
||||
// eslint-disable-next-line custom-rules/no-lookbehind-regex -- gated by text.includes('!`') below (PR#22986)
|
||||
const INLINE_PATTERN = /(?<=^|\s)!`([^`]+)`/gm
|
||||
|
||||
/**
|
||||
* Parses prompt text and executes any embedded shell commands.
|
||||
* Supports two syntaxes:
|
||||
* - Code blocks: ```! command ```
|
||||
* - Inline: !`command`
|
||||
*
|
||||
* @param shell - Shell to route commands through. Defaults to bash.
|
||||
* This is *never* read from settings.defaultShell — it comes from .md
|
||||
* frontmatter (author's choice) or is undefined for built-in commands.
|
||||
* See docs/design/ps-shell-selection.md §5.3.
|
||||
*/
|
||||
export async function executeShellCommandsInPrompt(
|
||||
text: string,
|
||||
context: ToolUseContext,
|
||||
slashCommandName: string,
|
||||
shell?: FrontmatterShell,
|
||||
): Promise<string> {
|
||||
let result = text
|
||||
|
||||
// Resolve the tool once. `shell === undefined` and `shell === 'bash'` both
|
||||
// hit BashTool. PowerShell only when the runtime gate allows — a skill
|
||||
// author's frontmatter choice doesn't override the user's opt-in/out.
|
||||
const shellTool: PromptShellTool =
|
||||
shell === 'powershell' && isPowerShellToolEnabled()
|
||||
? getPowerShellTool()
|
||||
: BashTool
|
||||
|
||||
// INLINE_PATTERN's lookbehind is ~100x slower than BLOCK_PATTERN on large
|
||||
// skill content (265µs vs 2µs @ 17KB). 93% of skills have no !` at all,
|
||||
// so gate the expensive scan on a cheap substring check. BLOCK_PATTERN
|
||||
// (```!) doesn't require !` in the text, so it's always scanned.
|
||||
const blockMatches = text.matchAll(BLOCK_PATTERN)
|
||||
const inlineMatches = text.includes('!`') ? text.matchAll(INLINE_PATTERN) : []
|
||||
|
||||
await Promise.all(
|
||||
[...blockMatches, ...inlineMatches].map(async match => {
|
||||
const command = match[1]?.trim()
|
||||
if (command) {
|
||||
try {
|
||||
// Check permissions before executing
|
||||
const permissionResult = await hasPermissionsToUseTool(
|
||||
shellTool,
|
||||
{ command },
|
||||
context,
|
||||
createAssistantMessage({ content: [] }),
|
||||
'',
|
||||
)
|
||||
|
||||
if (permissionResult.behavior !== 'allow') {
|
||||
logForDebugging(
|
||||
`Shell command permission check failed for command in ${slashCommandName}: ${command}. Error: ${permissionResult.message}`,
|
||||
)
|
||||
throw new MalformedCommandError(
|
||||
`Shell command permission check failed for pattern "${match[0]}": ${permissionResult.message || 'Permission denied'}`,
|
||||
)
|
||||
}
|
||||
|
||||
const { data } = await shellTool.call({ command }, context)
|
||||
// Reuse the same persistence flow as regular Bash tool calls
|
||||
const toolResultBlock = await processToolResultBlock(
|
||||
shellTool,
|
||||
data,
|
||||
randomUUID(),
|
||||
)
|
||||
// Extract the string content from the block
|
||||
const output =
|
||||
typeof toolResultBlock.content === 'string'
|
||||
? toolResultBlock.content
|
||||
: formatBashOutput(data.stdout, data.stderr)
|
||||
// Function replacer — String.replace interprets $$, $&, $`, $' in
|
||||
// the replacement string even with a string search pattern. Shell
|
||||
// output (especially PowerShell: $env:PATH, $$, $PSVersionTable)
|
||||
// is arbitrary user data; a bare string arg would corrupt it.
|
||||
result = result.replace(match[0], () => output)
|
||||
} catch (e) {
|
||||
if (e instanceof MalformedCommandError) {
|
||||
throw e
|
||||
}
|
||||
formatBashError(e, match[0])
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function formatBashOutput(
|
||||
stdout: string,
|
||||
stderr: string,
|
||||
inline = false,
|
||||
): string {
|
||||
const parts: string[] = []
|
||||
|
||||
if (stdout.trim()) {
|
||||
parts.push(stdout.trim())
|
||||
}
|
||||
|
||||
if (stderr.trim()) {
|
||||
if (inline) {
|
||||
parts.push(`[stderr: ${stderr.trim()}]`)
|
||||
} else {
|
||||
parts.push(`[stderr]\n${stderr.trim()}`)
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join(inline ? ' ' : '\n')
|
||||
}
|
||||
|
||||
function formatBashError(e: unknown, pattern: string, inline = false): never {
|
||||
if (e instanceof ShellError) {
|
||||
if (e.interrupted) {
|
||||
throw new MalformedCommandError(
|
||||
`Shell command interrupted for pattern "${pattern}": [Command interrupted]`,
|
||||
)
|
||||
}
|
||||
const output = formatBashOutput(e.stdout, e.stderr, inline)
|
||||
throw new MalformedCommandError(
|
||||
`Shell command failed for pattern "${pattern}": ${output}`,
|
||||
)
|
||||
}
|
||||
|
||||
const message = errorMessage(e)
|
||||
const formatted = inline ? `[Error: ${message}]` : `[Error]\n${message}`
|
||||
throw new MalformedCommandError(formatted)
|
||||
}
|
||||
Reference in New Issue
Block a user