2026-02-08 16:24:21 +09:00
import { basename } from "node:path"
2025-12-27 23:47:59 +09:00
import { pathToFileURL } from "node:url"
2025-12-27 17:56:40 +08:00
import { tool , type PluginInput , type ToolDefinition } from "@opencode-ai/plugin"
2025-12-13 15:25:29 +09:00
import { LOOK_AT_DESCRIPTION , MULTIMODAL_LOOKER_AGENT } from "./constants"
import type { LookAtArgs } from "./types"
2026-02-11 09:55:25 +09:00
import { log , promptSyncWithModelSuggestionRetry } from "../../shared"
2026-02-08 16:24:21 +09:00
import { extractLatestAssistantText } from "./assistant-message-extractor"
import type { LookAtArgsWithAlias } from "./look-at-arguments"
import { normalizeArgs , validateArgs } from "./look-at-arguments"
import {
extractBase64Data ,
inferMimeTypeFromBase64 ,
inferMimeTypeFromFilePath ,
} from "./mime-type-inference"
import { resolveMultimodalLookerAgentMetadata } from "./multimodal-agent-metadata"
2026-02-16 10:44:54 -08:00
import {
needsConversion ,
convertImageToJpeg ,
2026-02-16 11:08:25 -08:00
convertBase64ImageToJpeg ,
2026-02-16 10:44:54 -08:00
cleanupConvertedImage ,
} from "./image-converter"
2026-02-08 16:24:21 +09:00
2026-03-03 00:36:00 +09:00
function getTemporaryConversionPath ( error : unknown ) : string | null {
if ( ! ( error instanceof Error ) ) {
return null
}
const temporaryOutputPath = Reflect . get ( error , "temporaryOutputPath" )
if ( typeof temporaryOutputPath === "string" && temporaryOutputPath . length > 0 ) {
return temporaryOutputPath
}
const temporaryDirectory = Reflect . get ( error , "temporaryDirectory" )
if ( typeof temporaryDirectory === "string" && temporaryDirectory . length > 0 ) {
return temporaryDirectory
}
return null
}
2026-03-11 21:45:48 +09:00
2026-02-08 16:24:21 +09:00
export { normalizeArgs , validateArgs } from "./look-at-arguments"
2026-02-04 12:02:00 +08:00
2025-12-27 17:56:40 +08:00
export function createLookAt ( ctx : PluginInput ) : ToolDefinition {
2025-12-13 15:25:29 +09:00
return tool ( {
description : LOOK_AT_DESCRIPTION ,
args : {
2026-02-04 12:02:00 +08:00
file_path : tool.schema.string ( ) . optional ( ) . describe ( "Absolute path to the file to analyze" ) ,
image_data : tool.schema.string ( ) . optional ( ) . describe ( "Base64 encoded image data (for clipboard/pasted images)" ) ,
2025-12-13 15:25:29 +09:00
goal : tool.schema.string ( ) . describe ( "What specific information to extract from the file" ) ,
} ,
2026-01-15 14:49:56 +09:00
async execute ( rawArgs : LookAtArgs , toolContext ) {
const args = normalizeArgs ( rawArgs as LookAtArgsWithAlias )
const validationError = validateArgs ( args )
if ( validationError ) {
log ( ` [look_at] Validation failed: ${ validationError } ` )
return validationError
}
2026-02-04 12:02:00 +08:00
const isBase64Input = Boolean ( args . image_data )
const sourceDescription = isBase64Input ? "clipboard/pasted image" : args . file_path
log ( ` [look_at] Analyzing ${ sourceDescription } , goal: ${ args . goal } ` )
2026-02-08 16:24:21 +09:00
const imageData = args . image_data
const filePath = args . file_path
2026-02-04 12:02:00 +08:00
let mimeType : string
let filePart : { type : "file" ; mime : string ; url : string ; filename : string }
2026-02-16 10:44:54 -08:00
let tempFilePath : string | null = null
2026-03-03 00:36:00 +09:00
let tempConversionPath : string | null = null
2026-02-16 11:08:25 -08:00
let tempFilesToCleanup : string [ ] = [ ]
2025-12-13 15:25:29 +09:00
2026-02-16 10:44:54 -08:00
try {
if ( imageData ) {
2026-02-16 11:08:25 -08:00
mimeType = inferMimeTypeFromBase64 ( imageData )
let finalBase64Data = extractBase64Data ( imageData )
let finalMimeType = mimeType
if ( needsConversion ( mimeType ) ) {
log ( ` [look_at] Detected unsupported Base64 format: ${ mimeType } , converting to JPEG... ` )
try {
2026-02-17 08:21:07 -08:00
const { base64 , tempFiles } = convertBase64ImageToJpeg ( finalBase64Data , mimeType )
2026-02-16 11:08:25 -08:00
finalBase64Data = base64
finalMimeType = "image/jpeg"
tempFilesToCleanup = tempFiles
log ( ` [look_at] Base64 conversion successful ` )
} catch ( conversionError ) {
log ( ` [look_at] Base64 conversion failed: ${ conversionError } ` )
return ` Error: Failed to convert Base64 image format. ${ conversionError } `
}
}
filePart = {
type : "file" ,
mime : finalMimeType ,
url : ` data: ${ finalMimeType } ;base64, ${ finalBase64Data } ` ,
filename : ` clipboard-image. ${ finalMimeType . split ( "/" ) [ 1 ] || "png" } ` ,
}
} else if ( filePath ) {
2026-02-08 16:24:21 +09:00
mimeType = inferMimeTypeFromFilePath ( filePath )
2026-02-16 10:44:54 -08:00
let actualFilePath = filePath
if ( needsConversion ( mimeType ) ) {
log ( ` [look_at] Detected unsupported format: ${ mimeType } , converting to JPEG... ` )
try {
tempFilePath = convertImageToJpeg ( filePath , mimeType )
2026-03-03 00:36:00 +09:00
tempConversionPath = tempFilePath
2026-02-16 10:44:54 -08:00
actualFilePath = tempFilePath
mimeType = "image/jpeg"
log ( ` [look_at] Conversion successful: ${ tempFilePath } ` )
} catch ( conversionError ) {
2026-03-03 00:36:00 +09:00
const failedConversionPath = getTemporaryConversionPath ( conversionError )
if ( failedConversionPath ) {
tempConversionPath = failedConversionPath
}
2026-02-16 10:44:54 -08:00
log ( ` [look_at] Conversion failed: ${ conversionError } ` )
return ` Error: Failed to convert image format. ${ conversionError } `
}
}
2026-02-04 12:02:00 +08:00
filePart = {
type : "file" ,
mime : mimeType ,
2026-02-16 10:44:54 -08:00
url : pathToFileURL ( actualFilePath ) . href ,
filename : basename ( actualFilePath ) ,
2026-02-04 12:02:00 +08:00
}
2026-02-08 16:24:21 +09:00
} else {
return "Error: Must provide either 'file_path' or 'image_data'."
2026-02-04 12:02:00 +08:00
}
2025-12-23 11:22:59 +09:00
2026-04-07 15:10:40 +09:00
const readEnabled = false
const subjectNoun = isBase64Input ? "image" : "file"
const sourceClause = readEnabled
? ` Use the Read tool on the provided file path to load its contents, then analyze it. `
: ` The ${ subjectNoun } is already attached to this message. Analyze it directly from the attachment. Do NOT attempt to use the Read tool. The Read tool is disabled for this invocation and the ${ subjectNoun } cannot be loaded by path. `
const prompt = ` Analyze the attached ${ subjectNoun } and extract the requested information.
${ sourceClause }
2025-12-13 15:25:29 +09:00
Goal: ${ args . goal }
2025-12-23 11:22:59 +09:00
Provide ONLY the extracted information that matches the goal.
2025-12-13 15:25:29 +09:00
Be thorough on what was requested, concise on everything else.
If the requested information is not found, clearly state what is missing. `
2026-03-11 21:45:48 +09:00
const { agentModel , agentVariant } = await resolveMultimodalLookerAgentMetadata ( ctx )
2025-12-13 15:25:29 +09:00
log ( ` [look_at] Creating session with parent: ${ toolContext . sessionID } ` )
2026-01-13 06:27:56 +01:00
const parentSession = await ctx . client . session . get ( {
path : { id : toolContext.sessionID } ,
} ) . catch ( ( ) = > null )
const parentDirectory = parentSession ? . data ? . directory ? ? ctx . directory
2025-12-13 15:25:29 +09:00
const createResult = await ctx . client . session . create ( {
body : {
parentID : toolContext.sessionID ,
title : ` look_at: ${ args . goal . substring ( 0 , 50 ) } ` ,
2026-01-13 06:27:56 +01:00
} ,
2026-02-08 16:24:21 +09:00
query : { directory : parentDirectory } ,
2025-12-13 15:25:29 +09:00
} )
if ( createResult . error ) {
log ( ` [look_at] Session create error: ` , createResult . error )
2026-01-28 17:35:25 +09:00
const errorStr = String ( createResult . error )
if ( errorStr . toLowerCase ( ) . includes ( "unauthorized" ) ) {
return ` Error: Failed to create session (Unauthorized). This may be due to:
1. OAuth token restrictions (e.g., Claude Code credentials are restricted to Claude Code only)
2. Provider authentication issues
3. Session permission inheritance problems
Try using a different provider or API key authentication.
Original error: ${ createResult . error } `
}
2025-12-13 15:25:29 +09:00
return ` Error: Failed to create session: ${ createResult . error } `
}
const sessionID = createResult . data . id
log ( ` [look_at] Created session: ${ sessionID } ` )
2026-02-11 09:55:25 +09:00
log ( ` [look_at] Sending prompt with ${ isBase64Input ? "base64 image" : "file" } to session ${ sessionID } ` )
2026-01-28 23:58:01 +09:00
try {
2026-02-11 09:55:25 +09:00
await promptSyncWithModelSuggestionRetry ( ctx . client , {
2026-01-28 23:58:01 +09:00
path : { id : sessionID } ,
body : {
agent : MULTIMODAL_LOOKER_AGENT ,
tools : {
task : false ,
call_omo_agent : false ,
look_at : false ,
2026-04-07 15:10:40 +09:00
read : readEnabled ,
2026-01-28 23:58:01 +09:00
} ,
parts : [
{ type : "text" , text : prompt } ,
2026-02-04 12:02:00 +08:00
filePart ,
2026-01-28 23:58:01 +09:00
] ,
2026-03-11 21:56:19 +09:00
. . . ( agentModel ? { model : { providerID : agentModel.providerID , modelID : agentModel.modelID } } : { } ) ,
2026-01-30 16:57:13 +09:00
. . . ( agentVariant ? { variant : agentVariant } : { } ) ,
2025-12-13 15:25:29 +09:00
} ,
2026-01-28 23:58:01 +09:00
} )
} catch ( promptError ) {
2026-02-11 09:55:25 +09:00
log ( ` [look_at] Prompt error (ignored, will still fetch messages): ` , promptError )
2026-01-28 23:58:01 +09:00
}
2025-12-13 15:25:29 +09:00
2026-02-09 14:18:24 +09:00
log ( ` [look_at] Fetching messages from session ${ sessionID } ... ` )
2025-12-13 15:25:29 +09:00
const messagesResult = await ctx . client . session . messages ( {
path : { id : sessionID } ,
} )
if ( messagesResult . error ) {
log ( ` [look_at] Messages error: ` , messagesResult . error )
return ` Error: Failed to get messages: ${ messagesResult . error } `
}
const messages = messagesResult . data
log ( ` [look_at] Got ${ messages . length } messages ` )
2026-02-08 16:24:21 +09:00
const responseText = extractLatestAssistantText ( messages )
if ( ! responseText ) {
log ( "[look_at] No assistant message found" )
return "Error: No response from multimodal-looker agent"
2025-12-13 15:25:29 +09:00
}
2026-02-16 10:44:54 -08:00
log ( ` [look_at] Got response, length: ${ responseText . length } ` )
return responseText
2026-03-05 11:11:53 +09:00
} catch ( error ) {
const errorMessage = error instanceof Error ? error.message : String ( error )
log ( ` [look_at] Unexpected error analyzing ${ sourceDescription } : ` , error )
return ` Error: Failed to analyze ${ sourceDescription } : ${ errorMessage } `
2026-02-16 10:44:54 -08:00
} finally {
2026-03-03 00:36:00 +09:00
if ( tempConversionPath ) {
cleanupConvertedImage ( tempConversionPath )
} else if ( tempFilePath ) {
2026-02-16 10:44:54 -08:00
cleanupConvertedImage ( tempFilePath )
}
2026-03-03 00:36:00 +09:00
tempFilesToCleanup . forEach ( file = > {
cleanupConvertedImage ( file )
} )
2026-02-16 10:44:54 -08:00
}
2025-12-13 15:25:29 +09:00
} ,
} )
}