πŸ“„ File detail

tools/BriefTool/upload.ts

🧩 .tsπŸ“ 175 linesπŸ’Ύ 5,815 bytesπŸ“ text
← Back to All Files

🎯 Use case

This module implements the β€œBriefTool” tool (Brief) β€” something the model can call at runtime alongside other agent tools. On the API surface it exposes BriefUploadContext and uploadBriefAttachment β€” mainly functions, hooks, or classes. Dependencies touch bun:bundle, HTTP client, crypto, and Node filesystem. It composes internal code from bridge, constants, and utils (relative imports). What the file header says: Upload BriefTool attachments to private_api so web viewers can preview them. When the repl bridge is active, attachment paths are meaningless to a web viewer (they're on Claude's machine). We upload to /api/oauth/file_upload β€” the same store MessageComposer/SpaceMessage render fr.

Generated from folder role, exports, dependency roots, and inline comments β€” not hand-reviewed for every path.

🧠 Inline summary

Upload BriefTool attachments to private_api so web viewers can preview them. When the repl bridge is active, attachment paths are meaningless to a web viewer (they're on Claude's machine). We upload to /api/oauth/file_upload β€” the same store MessageComposer/SpaceMessage render from β€” and stash the returned file_uuid alongside the path. Web resolves file_uuid β†’ preview; desktop/local try path first. Best-effort: any failure (no token, bridge off, network error, 4xx) logs debug and returns undefined. The attachment still carries {path, size, isImage}, so local-terminal and same-machine-desktop render unaffected.

πŸ“€ Exports (heuristic)

  • BriefUploadContext
  • uploadBriefAttachment

πŸ“š External import roots

Package roots from from "…" (relative paths omitted).

  • bun:bundle
  • axios
  • crypto
  • fs
  • path
  • zod

πŸ–₯️ Source preview

/**
 * Upload BriefTool attachments to private_api so web viewers can preview them.
 *
 * When the repl bridge is active, attachment paths are meaningless to a web
 * viewer (they're on Claude's machine). We upload to /api/oauth/file_upload β€”
 * the same store MessageComposer/SpaceMessage render from β€” and stash the
 * returned file_uuid alongside the path. Web resolves file_uuid β†’ preview;
 * desktop/local try path first.
 *
 * Best-effort: any failure (no token, bridge off, network error, 4xx) logs
 * debug and returns undefined. The attachment still carries {path, size,
 * isImage}, so local-terminal and same-machine-desktop render unaffected.
 */

import { feature } from 'bun:bundle'
import axios from 'axios'
import { randomUUID } from 'crypto'
import { readFile } from 'fs/promises'
import { basename, extname } from 'path'
import { z } from 'zod/v4'

import {
  getBridgeAccessToken,
  getBridgeBaseUrlOverride,
} from '../../bridge/bridgeConfig.js'
import { getOauthConfig } from '../../constants/oauth.js'
import { logForDebugging } from '../../utils/debug.js'
import { lazySchema } from '../../utils/lazySchema.js'
import { jsonStringify } from '../../utils/slowOperations.js'

// Matches the private_api backend limit
const MAX_UPLOAD_BYTES = 30 * 1024 * 1024

const UPLOAD_TIMEOUT_MS = 30_000

// Backend dispatches on mime: image/* β†’ upload_image_wrapped (writes
// PREVIEW/THUMBNAIL, no ORIGINAL), everything else β†’ upload_generic_file
// (ORIGINAL only, no preview). Only whitelist raster formats the
// transcoder reliably handles β€” svg/bmp/ico risk a 400, and pdf routes
// to upload_pdf_file_wrapped which also skips ORIGINAL. Dispatch
// viewers use /preview for images and /contents for everything else,
// so images go image/* and the rest go octet-stream.
const MIME_BY_EXT: Record<string, string> = {
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.jpeg': 'image/jpeg',
  '.gif': 'image/gif',
  '.webp': 'image/webp',
}

function guessMimeType(filename: string): string {
  const ext = extname(filename).toLowerCase()
  return MIME_BY_EXT[ext] ?? 'application/octet-stream'
}

function debug(msg: string): void {
  logForDebugging(`[brief:upload] ${msg}`)
}

/**
 * Base URL for uploads. Must match the host the token is valid for.
 *
 * Subprocess hosts (cowork) pass ANTHROPIC_BASE_URL alongside
 * CLAUDE_CODE_OAUTH_TOKEN β€” prefer that since getOauthConfig() only
 * returns staging when USE_STAGING_OAUTH is set, which such hosts don't
 * set. Without this a staging token hits api.anthropic.com β†’ 401 β†’ silent
 * skip β†’ web viewer sees inert cards with no file_uuid.
 */
function getBridgeBaseUrl(): string {
  return (
    getBridgeBaseUrlOverride() ??
    process.env.ANTHROPIC_BASE_URL ??
    getOauthConfig().BASE_API_URL
  )
}

// /api/oauth/file_upload returns one of ChatMessage{Image,Blob,Document}FileSchema.
// All share file_uuid; that's the only field we need.
const uploadResponseSchema = lazySchema(() =>
  z.object({ file_uuid: z.string() }),
)

export type BriefUploadContext = {
  replBridgeEnabled: boolean
  signal?: AbortSignal
}

/**
 * Upload a single attachment. Returns file_uuid on success, undefined otherwise.
 * Every early-return is intentional graceful degradation.
 */
export async function uploadBriefAttachment(
  fullPath: string,
  size: number,
  ctx: BriefUploadContext,
): Promise<string | undefined> {
  // Positive pattern so bun:bundle eliminates the entire body from
  // non-BRIDGE_MODE builds (negative `if (!feature(...)) return` does not).
  if (feature('BRIDGE_MODE')) {
    if (!ctx.replBridgeEnabled) return undefined

    if (size > MAX_UPLOAD_BYTES) {
      debug(`skip ${fullPath}: ${size} bytes exceeds ${MAX_UPLOAD_BYTES} limit`)
      return undefined
    }

    const token = getBridgeAccessToken()
    if (!token) {
      debug('skip: no oauth token')
      return undefined
    }

    let content: Buffer
    try {
      content = await readFile(fullPath)
    } catch (e) {
      debug(`read failed for ${fullPath}: ${e}`)
      return undefined
    }

    const baseUrl = getBridgeBaseUrl()
    const url = `${baseUrl}/api/oauth/file_upload`
    const filename = basename(fullPath)
    const mimeType = guessMimeType(filename)
    const boundary = `----FormBoundary${randomUUID()}`

    // Manual multipart β€” same pattern as filesApi.ts. The oauth endpoint takes
    // a single "file" part (no "purpose" field like the public Files API).
    const body = Buffer.concat([
      Buffer.from(
        `--${boundary}\r\n` +
          `Content-Disposition: form-data; name="file"; filename="${filename}"\r\n` +
          `Content-Type: ${mimeType}\r\n\r\n`,
      ),
      content,
      Buffer.from(`\r\n--${boundary}--\r\n`),
    ])

    try {
      const response = await axios.post(url, body, {
        headers: {
          Authorization: `Bearer ${token}`,
          'Content-Type': `multipart/form-data; boundary=${boundary}`,
          'Content-Length': body.length.toString(),
        },
        timeout: UPLOAD_TIMEOUT_MS,
        signal: ctx.signal,
        validateStatus: () => true,
      })

      if (response.status !== 201) {
        debug(
          `upload failed for ${fullPath}: status=${response.status} body=${jsonStringify(response.data).slice(0, 200)}`,
        )
        return undefined
      }

      const parsed = uploadResponseSchema().safeParse(response.data)
      if (!parsed.success) {
        debug(
          `unexpected response shape for ${fullPath}: ${parsed.error.message}`,
        )
        return undefined
      }

      debug(`uploaded ${fullPath} β†’ ${parsed.data.file_uuid} (${size} bytes)`)
      return parsed.data.file_uuid
    } catch (e) {
      debug(`upload threw for ${fullPath}: ${e}`)
      return undefined
    }
  }
  return undefined
}