This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

add cdn links to discord turns

+104 -4
+10
src/discord/gateway.ts
··· 67 67 id: u.id, 68 68 bot: u.bot, 69 69 })), 70 + attachments: message.attachments.map((a) => ({ 71 + id: a.id, 72 + url: a.url, 73 + proxy_url: a.proxyURL, 74 + filename: a.name, 75 + content_type: a.contentType ?? null, 76 + width: a.width ?? null, 77 + height: a.height ?? null, 78 + size: a.size, 79 + })), 70 80 }, 71 81 channel: { 72 82 id: message.channelId,
+67
src/discord/parse.ts
··· 132 132 return new Set(parseChannelIds(input)) 133 133 } 134 134 135 + export type DiscordImageAttachment = { 136 + url: string 137 + filename: string | null 138 + contentType: string | null 139 + } 140 + 141 + const IMAGE_EXTENSION_RE = /\.(png|jpe?g|gif|webp|bmp|heic|heif|tiff?|avif)$/i 142 + 143 + /** 144 + * Determines whether a Discord attachment object is an image. 145 + * 146 + * @param attachment - Attachment object from a Discord message. 147 + * @returns `true` when the attachment is an image by content-type or filename. 148 + */ 149 + function isImageAttachment(attachment: DiscordObject): boolean { 150 + const contentType = asString(attachment.content_type) 151 + if (contentType && contentType.toLowerCase().startsWith("image/")) return true 152 + const name = asString(attachment.filename) ?? asString(attachment.url) 153 + if (!name) return false 154 + const pathPart = name.split("?")[0] ?? name 155 + return IMAGE_EXTENSION_RE.test(pathPart) 156 + } 157 + 158 + /** 159 + * Extracts image attachment CDN links from a Discord message object. 160 + * 161 + * Accepts either a raw event payload (with a `message` wrapper) or a message 162 + * object directly. Returns the Discord CDN url for each image attachment so the 163 + * agent can download it and run its own image tool on it. 164 + * 165 + * @param payload - Raw event payload or message object. 166 + * @returns Array of image attachments with CDN urls. 167 + */ 168 + export function extractImageAttachments(payload: unknown): DiscordImageAttachment[] { 169 + const root = asObject(payload) 170 + if (!root) return [] 171 + const message = asObject(root.message) ?? root 172 + const attachments = Array.isArray(message.attachments) ? message.attachments : [] 173 + const out: DiscordImageAttachment[] = [] 174 + for (const entry of attachments) { 175 + const attachment = asObject(entry) 176 + if (!attachment || !isImageAttachment(attachment)) continue 177 + const url = asString(attachment.url) ?? asString(attachment.proxy_url) 178 + if (!url) continue 179 + out.push({ 180 + url, 181 + filename: asString(attachment.filename), 182 + contentType: asString(attachment.content_type), 183 + }) 184 + } 185 + return out 186 + } 187 + 188 + /** 189 + * Extracts image attachment CDN links from a stored raw JSON payload. 190 + * 191 + * @param rawJson - Stored raw JSON for a Discord message. 192 + * @returns Array of image attachments, empty on parse failure. 193 + */ 194 + export function extractImageAttachmentsFromRawJson(rawJson: string): DiscordImageAttachment[] { 195 + try { 196 + return extractImageAttachments(JSON.parse(rawJson)) 197 + } catch { 198 + return [] 199 + } 200 + } 201 + 135 202 /** 136 203 * Parses a raw Discord gateway/webhook payload into a structured message record. 137 204 *
+15 -2
src/discord/state.ts
··· 11 11 import { 12 12 asNumber, 13 13 asString, 14 + extractImageAttachmentsFromRawJson, 14 15 parseChannelIds, 15 16 parseChannelRecord, 16 17 parseMessageRecord, ··· 116 117 const parsed = new Date(value) 117 118 if (Number.isNaN(parsed.getTime())) return value 118 119 return parsed.toISOString().replace("T", " ").replace(".000Z", "Z") 120 + } 121 + 122 + function formatBatchImages(rawJson: string): string { 123 + const images = extractImageAttachmentsFromRawJson(rawJson) 124 + if (images.length === 0) return "" 125 + return ` [images: ${images.map((img) => img.url).join(" ")}]` 119 126 } 120 127 121 128 function formatReplyContext(reply: DiscordReplyContext | undefined): string { ··· 349 356 350 357 const uniqueChannels = new Set(recentMessages.map((row) => row.channel_id)) 351 358 const replyContextByMessageId = buildReplyTargetContextMap([...recentMessages, ...pendingPreview]) 359 + const hasImages = [...recentMessages, ...pendingPreview].some( 360 + (row) => extractImageAttachmentsFromRawJson(row.raw_json).length > 0, 361 + ) 352 362 353 363 const lines: string[] = [ 354 364 `[discord batch] ${from} -> ${nowIso}`, ··· 356 366 `auto_seen_timeout=${autoSeenMinutes}m auto_demoted=${autoDemotedCount}`, 357 367 `channel_flag_repairs=${repairedMessageCount}`, 358 368 "channel messages are context, not direct requests. replying is optional; use judgment.", 369 + ...(hasImages 370 + ? ["image attachments appear as [images: <discord cdn urls>]; download with shell, then inspect with image_tool if useful."] 371 + : []), 359 372 "", 360 373 "recent messages:", 361 374 ] ··· 365 378 const author = row.author_username ? `@${row.author_username}` : "@unknown" 366 379 const ts = formatBatchTimestamp(row.created_at) 367 380 const replyTo = replyContextByMessageId.get(row.message_id) 368 - lines.push(`- [${label}] [${ts}] ${author}${formatReplyContext(replyTo)}: ${fullMessageText(row.content)}`) 381 + lines.push(`- [${label}] [${ts}] ${author}${formatReplyContext(replyTo)}: ${fullMessageText(row.content)}${formatBatchImages(row.raw_json)}`) 369 382 } 370 383 371 384 if (truncated) { ··· 382 395 const author = row.author_username ? `@${row.author_username}` : "@unknown" 383 396 const ts = formatBatchTimestamp(row.created_at) 384 397 const replyTo = replyContextByMessageId.get(row.message_id) 385 - lines.push(`- ${row.item_id} [${row.bucket}] [${label}] [${ts}] ${author}${formatReplyContext(replyTo)}: ${compactText(row.content, 120)}`) 398 + lines.push(`- ${row.item_id} [${row.bucket}] [${label}] [${ts}] ${author}${formatReplyContext(replyTo)}: ${compactText(row.content, 120)}${formatBatchImages(row.raw_json)}`) 386 399 } 387 400 } 388 401
+12 -2
src/triggers/discord.ts
··· 1 1 import type { UserMessage } from "../types" 2 2 import { getDb } from "../db" 3 - import { asObject, asString } from "../discord/parse" 3 + import { asObject, asString, extractImageAttachments } from "../discord/parse" 4 4 5 5 function asIsoTimestamp(value: unknown, fallback: string): string { 6 6 if (typeof value !== "string" && typeof value !== "number") return fallback ··· 58 58 return `reply_to: ${author} msg/${reply.message_id}: ${JSON.stringify(reply.content)}` 59 59 } 60 60 61 + function formatImageBlock(images: ReturnType<typeof extractImageAttachments>): string { 62 + if (images.length === 0) return "" 63 + const lines = images.map((img) => { 64 + const meta = [img.filename, img.contentType].filter(Boolean).join(", ") 65 + return `- ${img.url}${meta ? ` (${meta})` : ""}` 66 + }) 67 + return `\n\nimages (discord cdn links — download with shell, then inspect with image_tool if useful):\n${lines.join("\n")}` 68 + } 69 + 61 70 export function fromDiscord(body: unknown): UserMessage { 62 71 const b = body as Record<string, unknown> 63 72 const message = (typeof b.message === "object" && b.message ··· 98 107 ? "This is a direct message. Reply if it needs a response." 99 108 : "This is a server channel message, not a DM. You may choose not to reply; only respond if useful." 100 109 const replyLine = replyContextLine(message, b) 110 + const imageBlock = formatImageBlock(extractImageAttachments(body)) 101 111 102 112 return { 103 113 source: "discord", 104 114 triggeredAt, 105 - content: `${header} @${authorName}\ncontext: ${location}\nmessage_id: ${messageId}\ntimestamp: ${timestamp}${replyLine ? `\n${replyLine}` : ""}\naction: ${action}\n\n${content}`, 115 + content: `${header} @${authorName}\ncontext: ${location}\nmessage_id: ${messageId}\ntimestamp: ${timestamp}${replyLine ? `\n${replyLine}` : ""}\naction: ${action}\n\n${content}${imageBlock}`, 106 116 raw: body, 107 117 } 108 118 }