source dump of claude code
at main 224 lines 6.4 kB view raw
1import type { 2 ImageBlockParam, 3 TextBlockParam, 4 ToolResultBlockParam, 5} from '@anthropic-ai/sdk/resources/index.mjs' 6import { BASH_TOOL_NAME } from '../tools/BashTool/toolName.js' 7import { formatOutput } from '../tools/BashTool/utils.js' 8import type { 9 NotebookCell, 10 NotebookCellOutput, 11 NotebookCellSource, 12 NotebookCellSourceOutput, 13 NotebookContent, 14 NotebookOutputImage, 15} from '../types/notebook.js' 16import { getFsImplementation } from './fsOperations.js' 17import { expandPath } from './path.js' 18import { jsonParse } from './slowOperations.js' 19 20const LARGE_OUTPUT_THRESHOLD = 10000 21 22function isLargeOutputs( 23 outputs: (NotebookCellSourceOutput | undefined)[], 24): boolean { 25 let size = 0 26 for (const o of outputs) { 27 if (!o) continue 28 size += (o.text?.length ?? 0) + (o.image?.image_data.length ?? 0) 29 if (size > LARGE_OUTPUT_THRESHOLD) return true 30 } 31 return false 32} 33 34function processOutputText(text: string | string[] | undefined): string { 35 if (!text) return '' 36 const rawText = Array.isArray(text) ? text.join('') : text 37 const { truncatedContent } = formatOutput(rawText) 38 return truncatedContent 39} 40 41function extractImage( 42 data: Record<string, unknown>, 43): NotebookOutputImage | undefined { 44 if (typeof data['image/png'] === 'string') { 45 return { 46 image_data: data['image/png'].replace(/\s/g, ''), 47 media_type: 'image/png', 48 } 49 } 50 if (typeof data['image/jpeg'] === 'string') { 51 return { 52 image_data: data['image/jpeg'].replace(/\s/g, ''), 53 media_type: 'image/jpeg', 54 } 55 } 56 return undefined 57} 58 59function processOutput(output: NotebookCellOutput) { 60 switch (output.output_type) { 61 case 'stream': 62 return { 63 output_type: output.output_type, 64 text: processOutputText(output.text), 65 } 66 case 'execute_result': 67 case 'display_data': 68 return { 69 output_type: output.output_type, 70 text: processOutputText(output.data?.['text/plain']), 71 image: output.data && extractImage(output.data), 72 } 73 case 'error': 74 return { 75 output_type: output.output_type, 76 text: processOutputText( 77 `${output.ename}: ${output.evalue}\n${output.traceback.join('\n')}`, 78 ), 79 } 80 } 81} 82 83function processCell( 84 cell: NotebookCell, 85 index: number, 86 codeLanguage: string, 87 includeLargeOutputs: boolean, 88): NotebookCellSource { 89 const cellId = cell.id ?? `cell-${index}` 90 const cellData: NotebookCellSource = { 91 cellType: cell.cell_type, 92 source: Array.isArray(cell.source) ? cell.source.join('') : cell.source, 93 execution_count: 94 cell.cell_type === 'code' ? cell.execution_count || undefined : undefined, 95 cell_id: cellId, 96 } 97 // Avoid giving text cells the code language. 98 if (cell.cell_type === 'code') { 99 cellData.language = codeLanguage 100 } 101 102 if (cell.cell_type === 'code' && cell.outputs?.length) { 103 const outputs = cell.outputs.map(processOutput) 104 if (!includeLargeOutputs && isLargeOutputs(outputs)) { 105 cellData.outputs = [ 106 { 107 output_type: 'stream', 108 text: `Outputs are too large to include. Use ${BASH_TOOL_NAME} with: cat <notebook_path> | jq '.cells[${index}].outputs'`, 109 }, 110 ] 111 } else { 112 cellData.outputs = outputs 113 } 114 } 115 116 return cellData 117} 118 119function cellContentToToolResult(cell: NotebookCellSource): TextBlockParam { 120 const metadata = [] 121 if (cell.cellType !== 'code') { 122 metadata.push(`<cell_type>${cell.cellType}</cell_type>`) 123 } 124 if (cell.language !== 'python' && cell.cellType === 'code') { 125 metadata.push(`<language>${cell.language}</language>`) 126 } 127 const cellContent = `<cell id="${cell.cell_id}">${metadata.join('')}${cell.source}</cell id="${cell.cell_id}">` 128 return { 129 text: cellContent, 130 type: 'text', 131 } 132} 133 134function cellOutputToToolResult(output: NotebookCellSourceOutput) { 135 const outputs: (TextBlockParam | ImageBlockParam)[] = [] 136 if (output.text) { 137 outputs.push({ 138 text: `\n${output.text}`, 139 type: 'text', 140 }) 141 } 142 if (output.image) { 143 outputs.push({ 144 type: 'image', 145 source: { 146 data: output.image.image_data, 147 media_type: output.image.media_type, 148 type: 'base64', 149 }, 150 }) 151 } 152 return outputs 153} 154 155function getToolResultFromCell(cell: NotebookCellSource) { 156 const contentResult = cellContentToToolResult(cell) 157 const outputResults = cell.outputs?.flatMap(cellOutputToToolResult) 158 return [contentResult, ...(outputResults ?? [])] 159} 160 161/** 162 * Reads and parses a Jupyter notebook file into processed cell data 163 */ 164export async function readNotebook( 165 notebookPath: string, 166 cellId?: string, 167): Promise<NotebookCellSource[]> { 168 const fullPath = expandPath(notebookPath) 169 const buffer = await getFsImplementation().readFileBytes(fullPath) 170 const content = buffer.toString('utf-8') 171 const notebook = jsonParse(content) as NotebookContent 172 const language = notebook.metadata.language_info?.name ?? 'python' 173 if (cellId) { 174 const cell = notebook.cells.find(c => c.id === cellId) 175 if (!cell) { 176 throw new Error(`Cell with ID "${cellId}" not found in notebook`) 177 } 178 return [processCell(cell, notebook.cells.indexOf(cell), language, true)] 179 } 180 return notebook.cells.map((cell, index) => 181 processCell(cell, index, language, false), 182 ) 183} 184 185/** 186 * Maps notebook cell data to tool result block parameters with sophisticated text block merging 187 */ 188export function mapNotebookCellsToToolResult( 189 data: NotebookCellSource[], 190 toolUseID: string, 191): ToolResultBlockParam { 192 const allResults = data.flatMap(getToolResultFromCell) 193 194 // Merge adjacent text blocks 195 return { 196 tool_use_id: toolUseID, 197 type: 'tool_result' as const, 198 content: allResults.reduce<(TextBlockParam | ImageBlockParam)[]>( 199 (acc, curr) => { 200 if (acc.length === 0) return [curr] 201 202 const prev = acc[acc.length - 1] 203 if (prev && prev.type === 'text' && curr.type === 'text') { 204 // Merge the text blocks 205 prev.text += '\n' + curr.text 206 return acc 207 } 208 209 acc.push(curr) 210 return acc 211 }, 212 [], 213 ), 214 } 215} 216 217export function parseCellId(cellId: string): number | undefined { 218 const match = cellId.match(/^cell-(\d+)$/) 219 if (match && match[1]) { 220 const index = parseInt(match[1], 10) 221 return isNaN(index) ? undefined : index 222 } 223 return undefined 224}