{"record":{"id":"09e159ef5480b66e","repo":"can1357/oh-my-pi","slug":"images-urls-command-must-reference-file","errorCode":null,"errorMessage":"images.urls.command must reference {file}","messagePattern":"images\\.urls\\.command must reference (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/uploaders.ts","lineNumber":84,"sourceCode":"/** Last URL printed on stdout wins; uploader tools often log progress first. */\nexport function extractUploadUrl(stdout: string): string | null {\n\tlet last: string | null = null;\n\tfor (const match of stdout.matchAll(URL_PATTERN)) {\n\t\tlast = match[0].replace(/[)\\],.'\"]+$/, \"\");\n\t}\n\treturn last;\n}\n\n/**\n * Build an uploader from an argv template. Placeholders, substituted after\n * splitting (paths with spaces stay one argument): `{file}` temp file path,\n * `{mime}` MIME type, `{ext}` bare extension.\n */\nexport function createCommandUploader(template: string): BlobUploader {\n\tconst argvTemplate = splitCommandTemplate(template);\n\tif (argvTemplate.length === 0) throw new Error(\"images.urls.command is empty\");\n\tif (!argvTemplate.some(arg => arg.includes(\"{file}\"))) {\n\t\tthrow new Error(\"images.urls.command must reference {file}\");\n\t}\n\treturn {\n\t\tdestination: \"command\",\n\t\tasync upload(request: BlobUploadRequest): Promise<BlobPublication> {\n\t\t\tconst { bytes, mimeType, extension } = request;\n\t\t\tconst file = path.join(os.tmpdir(), `omp-blob-upload-${crypto.randomUUID()}.${extension}`);\n\t\t\tawait Bun.write(file, bytes);\n\t\t\ttry {\n\t\t\t\tconst argv = argvTemplate.map(arg =>\n\t\t\t\t\targ.replaceAll(\"{file}\", file).replaceAll(\"{mime}\", mimeType).replaceAll(\"{ext}\", extension),\n\t\t\t\t);\n\t\t\t\tconst cwd = getProjectDir();\n\t\t\t\tif (!directoryIsEnterableSync(cwd)) {\n\t\t\t\t\tthrow new Error(`Project directory is not accessible: ${cwd}`);\n\t\t\t\t}\n\t\t\t\tconst proc = Bun.spawn(argv, { stdin: \"ignore\", stdout: \"pipe\", stderr: \"pipe\", cwd });\n\t\t\t\tconst timeout = setTimeout(() => proc.kill(), UPLOAD_TIMEOUT_MS);\n\t\t\t\tconst [stdout, stderr, exitCode] = await Promise.all([","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/uploaders.ts#L66-L102","documentation":"createCommandUploader requires every command template to include the {file} placeholder, which is substituted with the temporary file path holding the upload bytes. Without {file} the command would never receive the blob to upload, so it throws at creation time. This enforces that the command actually transfers the payload.","triggerScenarios":"Configuring images.urls.command with a command lacking {file}, e.g. \"images.urls.command\": \"curl -d @- https://host\" or \"upload.sh\" — any template where no argument contains the literal {file}.","commonSituations":"Writing a command designed for stdin input instead of a file argument; forgetting the placeholder when adapting an existing upload script; copy-pasting a command from docs that reads the path from an environment variable instead.","solutions":["Add {file} to the command where the upload file path belongs, e.g. \"curl -F 'file=@{file}' https://0x0.st\"","If the tool reads from stdin, wrap it so it accepts a file argument (e.g. \"sh -c 'tool < {file}'\" won't work via placeholder substitution — use a tool that takes a path)","Confirm the placeholder braces are literal {file} (no extra spaces like { file })","Check splitting behavior: the placeholder can be inside a quoted argument"],"exampleFix":"// before\n\"urls\": { \"command\": \"transferwee https://transfer.sh\" }\n// after\n\"urls\": { \"command\": \"transferwee upload {file}\" }","handlingStrategy":"validation","validationCode":"const cmd = config.images?.urls?.command;\nif (typeof cmd === \"string\" && !cmd.includes(\"{file}\")) {\n\tthrow new Error(\"images.urls.command must reference {file}\");\n}","typeGuard":null,"tryCatchPattern":"try {\n\tconst uploader = createConfiguredUploader(\"command\", config);\n} catch (err) {\n\tif (err instanceof Error && err.message.includes(\"must reference {file}\")) {\n\t\t// rewrite the command to include {file} where the upload path belongs\n\t}\n\tthrow err;\n}","preventionTips":["Always include the literal {file} placeholder in command templates","Use tools that accept a file path argument (not stdin-only tools)","Double-check braces: {file} exactly, no spaces inside braces","Test the expanded command manually with a real file path before committing config"],"tags":["configuration","validation","command-uploader"],"backgroundTag":"missing-placeholder","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}