diff --git a/src/projections/activity.ts b/src/projections/activity.ts index 03e0681..ac210b2 100644 --- a/src/projections/activity.ts +++ b/src/projections/activity.ts @@ -30,6 +30,7 @@ export interface ActivityItem { } export interface ActivityPresentation { + renderer: "generic" | "telegram-message" | "bluesky-post"; title: string; body: string; objectLabel: string; @@ -37,6 +38,15 @@ export interface ActivityPresentation { urlLabel?: string; parentUrl?: string; parentLabel?: string; + telegram?: { + senderName: string; + edited: boolean; + attachmentCount: number; + }; + bluesky?: { + uri: string; + cid?: string; + }; } export interface RootActivityProjection { @@ -86,7 +96,7 @@ export async function buildRootActivity(store: JazzThoughtStore, limit = 100): P ...(event.parentEventId ? { parentEventId: event.parentEventId } : {}), privacy: event.privacy, summary: summarizeRoot(event), - presentation: presentRoot(event), + presentation: presentActivityEvent(event), descendantEventCount: descendantsByRoot.get(event.id) ?? 0, consumerRuns: (runsByRoot.get(event.id) ?? []).map((run) => ({ id: run.id, @@ -147,7 +157,7 @@ export async function buildRecentRootActivity( ...(event.parentEventId ? { parentEventId: event.parentEventId } : {}), privacy: event.privacy, summary: summarizeRoot(event), - presentation: presentRoot(event), + presentation: presentActivityEvent(event), descendantEventCount: descendantsByRoot.get(event.id)?.size ?? 0, consumerRuns: (runsByRoot.get(event.id) ?? []).map((run) => ({ id: run.id, @@ -210,7 +220,7 @@ export async function buildRecentSourceActivity( ...(event.parentEventId ? { parentEventId: event.parentEventId } : {}), privacy: event.privacy, summary: summarizeRoot(event), - presentation: presentRoot(event), + presentation: presentActivityEvent(event), descendantEventCount: consumerRuns.reduce((total, run) => total + run.outputCount + 1, 0), consumerRuns, }; @@ -294,7 +304,25 @@ function summarizeRoot(event: ThoughtEvent): string { return event.type; } -function presentRoot(event: ThoughtEvent): ActivityPresentation { +export function presentActivityEvent(event: ThoughtEvent): ActivityPresentation { + if (event.type === "stream.thought.source.telegram.message") { + const text = typeof event.payload.text === "string" ? event.payload.text : ""; + const senderName = typeof event.payload.senderName === "string" && event.payload.senderName.trim() + ? event.payload.senderName.trim() + : "Telegram sender"; + const attachments = Array.isArray(event.payload.attachments) ? event.payload.attachments : []; + return { + renderer: "telegram-message", + title: "Telegram message", + body: text, + objectLabel: attachments.length === 1 ? "1 attachment" : attachments.length > 1 ? `${attachments.length} attachments` : "Message", + telegram: { + senderName, + edited: typeof event.payload.editedAt === "string", + attachmentCount: attachments.length, + }, + }; + } if (event.type === "stream.thought.source.atproto.commit") { const payload = event.payload; const operation = typeof payload.operation === "string" ? payload.operation : "observed"; @@ -306,31 +334,45 @@ function presentRoot(event: ThoughtEvent): ActivityPresentation { ? typeof record.reply.parent.uri === "string" ? record.reply.parent.uri : undefined : undefined; const deleted = operation === "delete"; + const postUrl = atUri ? atUriToWebUrl(atUri) : ""; + const parentUrl = reply ? atUriToWebUrl(reply) : ""; return { + renderer: deleted || !postUrl ? "generic" : "bluesky-post", title: deleted ? "Deleted a Bluesky post" : reply ? "Replied on Bluesky" : "Posted on Bluesky", body: record && typeof record.text === "string" ? record.text : "", objectLabel: deleted ? "Post removed from Bluesky" : "Bluesky post", - ...(atUri ? { url: atUriToWebUrl(atUri), urlLabel: reply ? "Open reply" : "Open post" } : {}), - ...(reply ? { parentUrl: atUriToWebUrl(reply), parentLabel: "Open parent post" } : {}), + ...(postUrl ? { url: postUrl, urlLabel: reply ? "Open reply" : "Open post" } : {}), + ...(parentUrl ? { parentUrl, parentLabel: "Open parent post" } : {}), + ...(!deleted && postUrl && atUri ? { bluesky: { + uri: atUri, + ...(typeof payload.cid === "string" ? { cid: payload.cid } : {}), + } } : {}), }; } if (collection === "app.bsky.feed.like" || collection === "app.bsky.feed.repost") { const subject = record && isObject(record.subject) && typeof record.subject.uri === "string" ? record.subject.uri : undefined; + const subjectUrl = subject ? atUriToWebUrl(subject) : ""; const like = collection.endsWith("like"); const removed = operation === "delete"; return { + renderer: removed || !subjectUrl ? "generic" : "bluesky-post", title: like ? removed ? "Removed a like" : "Liked a post" : removed ? "Removed a repost" : "Reposted on Bluesky", body: "", objectLabel: "Bluesky post", - ...(subject ? { url: atUriToWebUrl(subject), urlLabel: like && !removed ? "Open liked post" : "Open post" } : {}), + ...(subjectUrl ? { url: subjectUrl, urlLabel: like && !removed ? "Open liked post" : "Open post" } : {}), + ...(!removed && subjectUrl && subject ? { bluesky: { + uri: subject, + ...(record && isObject(record.subject) && typeof record.subject.cid === "string" ? { cid: record.subject.cid } : {}), + } } : {}), }; } if (collection === "app.bsky.graph.follow") { const subject = record && typeof record.subject === "string" ? record.subject : undefined; const removed = operation === "delete"; return { + renderer: "generic", title: removed ? "Stopped following an account" : "Followed an account", body: "", objectLabel: "Bluesky account", @@ -340,6 +382,7 @@ function presentRoot(event: ThoughtEvent): ActivityPresentation { } const summary = summarizeRoot(event); return { + renderer: "generic", title: event.type === "stream.thought.source.x.activity" ? "X activity" : capitalize(event.type.split(".").at(-1) ?? "observation"), body: summary === event.type ? "" : summary, objectLabel: "Observation", @@ -368,5 +411,7 @@ function truncate(value: string, limit: number): string { function atUriToWebUrl(value: string): string { const match = value.match(/^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/([^/]+)$/); - return match ? `https://bsky.app/profile/${match[1]}/post/${match[2]}` : value; + return match + ? `https://bsky.app/profile/${encodeURIComponent(match[1]!)}/post/${encodeURIComponent(match[2]!)}` + : ""; } diff --git a/src/web/inspector.ts b/src/web/inspector.ts index 0198825..ed155e8 100644 --- a/src/web/inspector.ts +++ b/src/web/inspector.ts @@ -1,6 +1,11 @@ import http, { type IncomingMessage, type ServerResponse } from "node:http"; import type { JazzThoughtStore } from "../jazz/store.js"; -import { buildRecentRootActivity, buildRecentSourceActivity, describeRunResult } from "../projections/activity.js"; +import { + buildRecentRootActivity, + buildRecentSourceActivity, + describeRunResult, + presentActivityEvent, +} from "../projections/activity.js"; import { buildSourceHealth } from "../projections/source-health.js"; import { auditRunEvidence } from "../agents/evidence.js"; import type { AgentRun } from "../store/types.js"; @@ -34,6 +39,52 @@ const RUN_TERMINAL_EVENT_TYPES = [ "stream.thought.agent.run.blocked", "stream.thought.agent.run.abandoned", ]; +const BLUESKY_POST_URI_PATTERN = /^at:\/\/[^/]{3,256}\/app\.bsky\.feed\.post\/[a-zA-Z0-9._~:-]{1,256}$/; +const SAFE_CID_PATTERN = /^[a-zA-Z0-9]{8,128}$/; +const BLUESKY_INLAY_CACHE_TTL_MS = 10 * 60_000; + +interface BlueskyPostInlay { + uri: string; + cid: string; + url: string; + author: { + displayName: string; + handle: string; + avatar?: string; + }; + text: string; + createdAt?: string; + images: Array<{ thumb: string; fullsize: string; alt: string }>; + external?: { uri: string; title: string; description: string; thumb?: string }; + quote?: { + uri: string; + url: string; + author: { displayName: string; handle: string; avatar?: string }; + text: string; + }; + counts: { replies: number; reposts: number; likes: number }; +} + +const blueskyPostViewSchema = z.object({ + uri: z.string(), + cid: z.string(), + author: z.object({ + handle: z.string(), + displayName: z.string().optional(), + avatar: z.string().url().optional(), + }).passthrough(), + record: z.object({ + text: z.string().optional(), + createdAt: z.string().optional(), + }).passthrough(), + embed: z.unknown().optional(), + replyCount: z.number().int().nonnegative().optional(), + repostCount: z.number().int().nonnegative().optional(), + likeCount: z.number().int().nonnegative().optional(), +}).passthrough(); +const blueskyPostResponseSchema = z.object({ posts: z.array(blueskyPostViewSchema) }); +const blueskyInlayCache = new Map(); +let aroundFontCache: Buffer | undefined; export interface InspectorServerOptions { host?: string; @@ -160,6 +211,29 @@ async function handleRequest( send(response, 200, "text/html; charset=utf-8", renderInspectorHtml()); return; } + if (url.pathname === "/assets/around-regular.woff2") { + try { + aroundFontCache ??= await loadAroundFont(); + sendBytes(response, 200, "font/woff2", aroundFontCache, "public, max-age=31536000, immutable"); + } catch { + sendJson(response, 502, { error: "Display font is unavailable" }); + } + return; + } + if (url.pathname === "/api/inlays/bluesky") { + const uri = url.searchParams.get("uri"); + const cid = url.searchParams.get("cid") ?? undefined; + if (!uri || !BLUESKY_POST_URI_PATTERN.test(uri) || (cid !== undefined && !SAFE_CID_PATTERN.test(cid))) { + sendJson(response, 400, { error: "Bluesky post reference is invalid" }); + return; + } + try { + sendJson(response, 200, await loadBlueskyPostInlay(uri, cid)); + } catch { + sendJson(response, 502, { error: "Bluesky post is unavailable" }); + } + return; + } if (url.pathname === "/api/snapshot") { const [runs, agents, sources] = await Promise.all([ store.listRuns(), @@ -268,6 +342,7 @@ async function handleRequest( const relevantRuns = runs.filter((run) => eventBelongsToRun(event, run, runInputsById)); sendJson(response, 200, { event, + presentation: presentActivityEvent(event), parent, children, root, @@ -510,49 +585,191 @@ function escapeArtifactHtml(value: string): string { })[character]!); } +async function loadBlueskyPostInlay(uri: string, expectedCid?: string): Promise { + const cacheKey = `${uri}\u0000${expectedCid ?? "current"}`; + const cached = blueskyInlayCache.get(cacheKey); + if (cached && cached.expiresAt > Date.now()) return cached.value; + blueskyInlayCache.delete(cacheKey); + + const endpoint = new URL("https://public.api.bsky.app/xrpc/app.bsky.feed.getPosts"); + endpoint.searchParams.append("uris", uri); + const response = await fetch(endpoint, { + headers: { accept: "application/json" }, + signal: AbortSignal.timeout(4_000), + }); + if (!response.ok) throw new Error(`Bluesky AppView returned ${response.status}`); + const parsed = blueskyPostResponseSchema.parse(await response.json()); + const post = parsed.posts.find((candidate) => candidate.uri === uri); + if (!post || (expectedCid && post.cid !== expectedCid)) throw new Error("Bluesky post did not match the observed strong reference"); + + const embed = blueskyEmbed(post.embed); + const value: BlueskyPostInlay = { + uri: post.uri, + cid: post.cid, + url: atUriToBlueskyUrl(post.uri), + author: blueskyAuthor(post.author), + text: post.record.text ?? "", + ...(post.record.createdAt ? { createdAt: post.record.createdAt } : {}), + images: embed.images, + ...(embed.external ? { external: embed.external } : {}), + ...(embed.quote ? { quote: embed.quote } : {}), + counts: { + replies: post.replyCount ?? 0, + reposts: post.repostCount ?? 0, + likes: post.likeCount ?? 0, + }, + }; + blueskyInlayCache.set(cacheKey, { expiresAt: Date.now() + BLUESKY_INLAY_CACHE_TTL_MS, value }); + while (blueskyInlayCache.size > 512) { + const oldest = blueskyInlayCache.keys().next().value as string | undefined; + if (!oldest) break; + blueskyInlayCache.delete(oldest); + } + return value; +} + +async function loadAroundFont(): Promise { + const response = await fetch("https://cameron.stream/public/fonts/around-regular.woff2?v=20260712-12", { + headers: { accept: "font/woff2" }, + signal: AbortSignal.timeout(4_000), + }); + if (!response.ok) throw new Error(`Cameron.stream returned ${response.status}`); + const bytes = Buffer.from(await response.arrayBuffer()); + if (bytes.length < 4 || bytes.length > 65_536 || bytes.subarray(0, 4).toString("ascii") !== "wOF2") { + throw new Error("Cameron.stream returned an invalid Around font"); + } + return bytes; +} + +function blueskyEmbed(value: unknown): Pick { + const embed = objectValue(value); + const type = stringValue(embed?.$type); + const media = type === "app.bsky.embed.recordWithMedia#view" ? objectValue(embed?.media) : embed; + const images = Array.isArray(media?.images) + ? media.images.flatMap((candidate) => { + const image = objectValue(candidate); + const thumb = safeHttpsUrl(image?.thumb); + const fullsize = safeHttpsUrl(image?.fullsize); + if (!thumb || !fullsize) return []; + return [{ thumb, fullsize, alt: stringValue(image?.alt) ?? "" }]; + }) + : []; + const externalView = objectValue(media?.external); + const externalUri = safeHttpUrl(externalView?.uri); + const externalThumb = safeHttpsUrl(externalView?.thumb); + const external = externalUri + ? { + uri: externalUri, + title: stringValue(externalView?.title) ?? externalUri, + description: stringValue(externalView?.description) ?? "", + ...(externalThumb ? { thumb: externalThumb } : {}), + } + : undefined; + const recordContainer = type === "app.bsky.embed.recordWithMedia#view" ? objectValue(embed?.record) : embed; + const recordView = objectValue(recordContainer?.record); + const quoteAuthor = objectValue(recordView?.author); + const quoteValue = objectValue(recordView?.value); + const quoteUri = stringValue(recordView?.uri); + const quote = quoteUri && BLUESKY_POST_URI_PATTERN.test(quoteUri) && quoteAuthor && quoteValue + ? { + uri: quoteUri, + url: atUriToBlueskyUrl(quoteUri), + author: blueskyAuthor(quoteAuthor), + text: stringValue(quoteValue.text) ?? "", + } + : undefined; + return { images, ...(external ? { external } : {}), ...(quote ? { quote } : {}) }; +} + +function blueskyAuthor(value: Record): BlueskyPostInlay["author"] { + const handle = stringValue(value.handle) ?? "unknown.handle"; + return { + displayName: stringValue(value.displayName)?.trim() || handle, + handle, + ...(safeHttpsUrl(value.avatar) ? { avatar: safeHttpsUrl(value.avatar)! } : {}), + }; +} + +function objectValue(value: unknown): Record | undefined { + return value !== null && typeof value === "object" && !Array.isArray(value) + ? value as Record + : undefined; +} + +function stringValue(value: unknown): string | undefined { + return typeof value === "string" ? value : undefined; +} + +function safeHttpsUrl(value: unknown): string | undefined { + const url = safeHttpUrl(value); + return url?.startsWith("https://") ? url : undefined; +} + +function safeHttpUrl(value: unknown): string | undefined { + if (typeof value !== "string") return undefined; + try { + const url = new URL(value); + return url.protocol === "https:" || url.protocol === "http:" ? url.toString() : undefined; + } catch { + return undefined; + } +} + +function atUriToBlueskyUrl(uri: string): string { + const match = uri.match(BLUESKY_POST_URI_PATTERN); + if (!match) return uri; + const parts = uri.slice(5).split("/"); + return `https://bsky.app/profile/${encodeURIComponent(parts[0]!)}/post/${encodeURIComponent(parts[2]!)}`; +} + export function renderInspectorHtml(): string { return ` - thought stream inspector + The Stream -

thought stream

+

The Stream

recent activity
Loading recent activity…