diff --git a/apps/evi/agent/channels/linear.ts b/apps/evi/agent/channels/linear.ts index b2245215..4530ee6d 100644 --- a/apps/evi/agent/channels/linear.ts +++ b/apps/evi/agent/channels/linear.ts @@ -1,6 +1,29 @@ import { connectLinearCredentials } from '@vercel/connect/eve' import { linearChannel } from 'eve/channels/linear' +import { captureReaction } from '../lib/feedback' export default linearChannel({ credentials: connectLinearCredentials('linear/evi'), + // A thumbs up/down reaction on a comment becomes a verdict on that message. + // Only fires when the Linear webhook subscription for this app includes the + // CommentReaction type; when it does not, no reaction webhooks arrive and + // this handler is a silent no-op, so the channel keeps working either way. + onDataWebhook: async (event) => { + if (event.type !== 'CommentReaction' || event.action !== 'create') return + const data = (event.raw?.data ?? event.raw) as Record | undefined + const emoji = typeof data?.reactionEmoji === 'string' + ? data.reactionEmoji + : typeof data?.emoji === 'string' + ? data.emoji + : '' + const commentId = typeof data?.commentId === 'string' ? data.commentId : undefined + const userId = typeof data?.userId === 'string' ? data.userId : undefined + await captureReaction({ + channel: 'linear', + emoji, + author: userId ?? 'linear:unknown', + added: true, + messageRef: commentId, + }) + }, }) diff --git a/apps/evi/agent/channels/photon.ts b/apps/evi/agent/channels/photon.ts index 0ed0cb14..bfd1eb82 100644 --- a/apps/evi/agent/channels/photon.ts +++ b/apps/evi/agent/channels/photon.ts @@ -1,7 +1,22 @@ import { connectPhotonCredentials } from '@vercel/connect/eve' import { photonIMessageChannel } from 'eve/channels/photon' +import { captureReaction } from '../lib/feedback' import { MAINTAINER_PHONE } from '../lib/trust' +/** + * The inbound reaction (tapback) the repo's eve patch bridges out of the + * Chat SDK. Kept local so the patch does not have to widen the package's + * public index. + */ +interface PhotonReactionEvent { + emoji: string + rawEmoji?: string + added: boolean + messageId: string + threadId: string + userName: string +} + const MAX_VALUE_LENGTH = 160 function formatValue(value: unknown) { @@ -25,6 +40,17 @@ export default photonIMessageChannel({ }, } }, + // A thumbs up/down tapback on an iMessage becomes a verdict on that message. + onReaction: async (reaction: PhotonReactionEvent) => { + await captureReaction({ + channel: 'imessage', + emoji: reaction.emoji, + author: reaction.userName || 'imessage:unknown', + added: reaction.added, + messageRef: reaction.messageId, + threadRef: reaction.threadId, + }) + }, events: { async 'input.requested'(event, channel) { if (!channel.thread || event.requests.length === 0) return diff --git a/apps/evi/agent/lib/feedback.test.ts b/apps/evi/agent/lib/feedback.test.ts new file mode 100644 index 00000000..9f29564b --- /dev/null +++ b/apps/evi/agent/lib/feedback.test.ts @@ -0,0 +1,83 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { captureReaction, feedbackStats, NO_DB_ERROR, saveFeedback, verdictFromReactionEmoji } from './feedback' + +const NAMES = ['DATABASE_URL', 'POSTGRES_URL', 'POSTGRESQL_URL'] as const + +describe('verdictFromReactionEmoji', () => { + it('maps positive reactions and their aliases', () => { + for (const emoji of ['👍', '+1', 'thumbs_up', 'thumbsup', 'THUMBS-UP']) { + expect(verdictFromReactionEmoji(emoji)).toBe('positive') + } + }) + + it('maps negative reactions and their aliases', () => { + for (const emoji of ['👎', '-1', 'thumbs_down', 'thumbsdown']) { + expect(verdictFromReactionEmoji(emoji)).toBe('negative') + } + }) + + it('ignores reactions that are not a thumb', () => { + for (const emoji of ['❤️', '👀', '🚀', '', 'rocket', 'eyes']) { + expect(verdictFromReactionEmoji(emoji)).toBeNull() + } + }) +}) + +describe('captureReaction', () => { + afterEach(() => { + for (const name of NAMES) delete process.env[name] + }) + + it('returns null for a reaction that is not a thumb', async () => { + for (const name of NAMES) delete process.env[name] + expect(await captureReaction({ channel: 'imessage', emoji: '🚀', author: 'imessage:x', added: true })).toBeNull() + }) + + it('returns null for a removal so a capture source no-ops', async () => { + for (const name of NAMES) delete process.env[name] + expect(await captureReaction({ channel: 'imessage', emoji: '👍', author: 'imessage:x', added: false })).toBeNull() + }) + + it('records a verdict through saveFeedback when the emoji is a thumb', async () => { + for (const name of NAMES) delete process.env[name] + const result = await captureReaction({ channel: 'linear', emoji: 'thumbs_down', author: 'linear:user', added: true }) + expect(result).toEqual({ success: false, error: NO_DB_ERROR }) + }) +}) + +describe('saveFeedback', () => { + afterEach(() => { + for (const name of NAMES) delete process.env[name] + }) + + it('degrades with an unavailable error when no database is configured', async () => { + for (const name of NAMES) delete process.env[name] + const result = await saveFeedback({ + channel: 'linear', + verdict: 'negative', + author: 'linear:user', + source: 'reaction', + }) + expect(result).toEqual({ success: false, error: NO_DB_ERROR }) + }) +}) + +describe('feedbackStats', () => { + afterEach(() => { + for (const name of NAMES) delete process.env[name] + }) + + it('reports an empty store when no database is configured', async () => { + for (const name of NAMES) delete process.env[name] + const stats = await feedbackStats(new Date()) + expect(stats).toEqual({ + total: 0, + positive: 0, + negative: 0, + reactions: 0, + written: 0, + byChannel: {}, + recentNegativeWritten: [], + }) + }) +}) diff --git a/apps/evi/agent/lib/feedback.ts b/apps/evi/agent/lib/feedback.ts new file mode 100644 index 00000000..52a7ef01 --- /dev/null +++ b/apps/evi/agent/lib/feedback.ts @@ -0,0 +1,157 @@ +import { desc, gt } from 'drizzle-orm' +import { feedback } from '../../db/schema' +import { getDb, isDbConfigured } from './db' + +export type FeedbackVerdict = 'positive' | 'negative' +export type FeedbackSource = 'reaction' | 'written' + +export interface SaveFeedbackInput { + channel: string + verdict: FeedbackVerdict + author: string + source: FeedbackSource + text?: string + messageRef?: string + threadRef?: string + sessionRef?: string +} + +export type SaveFeedbackResult = + | { success: true; id: string } + | { success: false; error: string } + +/** Shown to a caller when the store is absent; local runs without the DB keep working. */ +export const NO_DB_ERROR = 'The feedback store is not configured in this environment.' + +const POSITIVE_REACTIONS = new Set(['👍', '+1', 'thumbs_up', 'thumbsup', 'thumbs-up']) +const NEGATIVE_REACTIONS = new Set(['👎', '-1', 'thumbs_down', 'thumbsdown', 'thumbs-down']) + +/** + * Maps a reaction emoji (or its common shortcode aliases) to a binary verdict. + * Returns null for anything that is neither a thumb up nor a thumb down, so + * unrelated reactions (hearts, eyes, ...) are ignored by the capture. + */ +export function verdictFromReactionEmoji(emoji: string): FeedbackVerdict | null { + const normalized = emoji.trim().toLowerCase() + if (POSITIVE_REACTIONS.has(normalized)) return 'positive' + if (NEGATIVE_REACTIONS.has(normalized)) return 'negative' + return null +} + +/** + * Shared entry point for every reaction path (iMessage tapback, Linear webhook, + * future GitHub). Maps the emoji to a verdict and records it. Returns null when + * the emoji is not a thumb or when the event is a removal, so a capture source + * treats "nothing to store" as a no-op rather than an error. + */ +export async function captureReaction( + input: Omit & { emoji: string; added?: boolean }, +): Promise { + if (input.added === false) return null + const verdict = verdictFromReactionEmoji(input.emoji) + if (!verdict) return null + return await saveFeedback({ ...input, verdict, source: 'reaction' }) +} + +/** + * Persists one feedback row. Returns an unavailable error when no database is + * configured rather than throwing, mirroring the `isDbConfigured()` guard the + * rest of the agent uses so a feature degrades instead of crashing. + */ +export async function saveFeedback(input: SaveFeedbackInput): Promise { + if (!isDbConfigured()) return { success: false, error: NO_DB_ERROR } + const db = getDb() + if (!db) return { success: false, error: NO_DB_ERROR } + try { + const rows = await db + .insert(feedback) + .values({ + channel: input.channel, + verdict: input.verdict, + author: input.author, + source: input.source, + text: input.text ?? null, + messageRef: input.messageRef ?? null, + threadRef: input.threadRef ?? null, + sessionRef: input.sessionRef ?? null, + }) + .returning({ id: feedback.id }) + return { success: true, id: rows[0]?.id ?? '' } + } catch (error) { + console.error('[evi:feedback] failed to save feedback', error) + return { success: false, error: 'Feedback could not be stored.' } + } +} + +/** A stored feedback row as read back from the store. */ +export interface FeedbackRow { + id: string + channel: string + source: string + verdict: string + author: string + text: string | null + messageRef: string | null + threadRef: string | null + sessionRef: string | null + createdAt: Date +} + +/** + * Rows created since `since`, newest first. Degrades to an empty list when the + * store is not configured, so callers (stats, the weekly review) treat a + * missing DB the same as "no feedback yet". + */ +export async function listFeedbackSince(since: Date): Promise { + if (!isDbConfigured()) return [] + const db = getDb() + if (!db) return [] + return await db + .select() + .from(feedback) + .where(gt(feedback.createdAt, since)) + .orderBy(desc(feedback.createdAt)) +} + +/** Aggregates over the rows captured since `since`, for the admin stats tool. */ +export interface FeedbackStats { + total: number + positive: number + negative: number + reactions: number + written: number + byChannel: Record + /** The most recent written negative feedback with reasons, for the weekly review. */ + recentNegativeWritten: { + id: string + channel: string + text: string + createdAt: Date + messageRef: string | null + }[] +} + +const RECENT_NEGATIVE_LIMIT = 10 + +export async function feedbackStats(since: Date): Promise { + const rows = await listFeedbackSince(since) + const byChannel: Record = {} + let positive = 0 + let negative = 0 + let reactions = 0 + let written = 0 + const recentNegativeWritten: FeedbackStats['recentNegativeWritten'] = [] + + for (const row of rows) { + byChannel[row.channel] = (byChannel[row.channel] ?? 0) + 1 + if (row.verdict === 'positive') positive += 1 + else negative += 1 + if (row.source === 'reaction') reactions += 1 + else written += 1 + if (row.source === 'written' && row.verdict === 'negative' && row.text && recentNegativeWritten.length < RECENT_NEGATIVE_LIMIT) { + recentNegativeWritten.push({ id: row.id, channel: row.channel, text: row.text, createdAt: row.createdAt, messageRef: row.messageRef }) + } + } + + return { total: rows.length, positive, negative, reactions, written, byChannel, recentNegativeWritten } +} diff --git a/apps/evi/agent/tools/feedback.ts b/apps/evi/agent/tools/feedback.ts new file mode 100644 index 00000000..af3ba522 --- /dev/null +++ b/apps/evi/agent/tools/feedback.ts @@ -0,0 +1,69 @@ +import { defineDynamic, defineTool } from 'eve/tools' +import { z } from 'zod' +import { channelName } from '../lib/channel' +import { feedbackStats, NO_DB_ERROR, saveFeedback } from '../lib/feedback' +import { canAccessAdminTools, isAutonomous } from '../lib/trust' + +// Feedback capture is a small, user-requested write: it stays off the approval +// cards. Autonomous first-responder turns never see it (they process untrusted +// text and have no one to confirm with). The stats tool is admin-only. Keep +// executes inline in the resolver (docs/notes.md). +export default defineDynamic({ + events: { + 'turn.started': (_event, ctx) => { + if (isAutonomous(ctx.session.auth.current)) return null + const channel = channelName(ctx.channel.kind) + const sessionRef = ctx.session.id + const admin = canAccessAdminTools(ctx.session.auth.current) + return { + feedback__record: defineTool({ + description: + 'Record written feedback about an Evi answer on behalf of the person in this conversation. Call it only when that person explicitly gives feedback (for example "this answer was wrong because X" or "that was really helpful"), never to record your own judgement. The entry is stored in the feedback store and feeds the weekly self-review and evals. Capture their reason as text when they give one.', + inputSchema: z.object({ + verdict: z.enum(['positive', 'negative']).describe('Whether the feedback is positive or negative.'), + text: z.string().min(1).describe('What the person said about the answer, in their words, including the reason.'), + messageRef: z.string().optional().describe('Optional platform identifier of the specific message or comment being rated.'), + }), + async execute(input, toolCtx) { + if (isAutonomous(toolCtx.session.auth.current)) { + return { success: false as const, error: 'Feedback capture is not available in this session.' } + } + const author = toolCtx.session.auth.current?.principalId ?? 'unknown' + const result = await saveFeedback({ + channel, + verdict: input.verdict, + author, + text: input.text, + messageRef: input.messageRef, + sessionRef, + source: 'written', + }) + if (!result.success && result.error === NO_DB_ERROR) { + return { success: false as const, error: NO_DB_ERROR } + } + return result + }, + }), + ...(admin + ? { + feedback__stats: defineTool({ + description: + 'Read feedback statistics from the store since a number of days ago: totals, positive/negative split, reaction vs written counts, per-channel counts, and the most recent written negative feedback with its reasons. Use it to review how Evi is doing, or as input to the weekly self-review or evals.', + inputSchema: z.object({ + sinceDays: z.number().int().min(1).max(365).default(7).describe('How far back to aggregate, in days. Defaults to 7.'), + }), + async execute(input, toolCtx) { + if (!canAccessAdminTools(toolCtx.session.auth.current)) { + return { success: false as const, error: 'Feedback statistics are only available to admin sessions.' } + } + const since = new Date(Date.now() - input.sinceDays * 24 * 60 * 60 * 1000) + const stats = await feedbackStats(since) + return { success: true as const, sinceDays: input.sinceDays, ...stats } + }, + }), + } + : {}), + } + }, + }, +}) diff --git a/apps/evi/db/migrations/0000_silly_pixie.sql b/apps/evi/db/migrations/0000_silly_pixie.sql new file mode 100644 index 00000000..05721023 --- /dev/null +++ b/apps/evi/db/migrations/0000_silly_pixie.sql @@ -0,0 +1,14 @@ +CREATE TABLE "feedback" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "channel" text NOT NULL, + "source" text NOT NULL, + "verdict" text NOT NULL, + "author" text NOT NULL, + "text" text, + "message_ref" text, + "thread_ref" text, + "session_ref" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE INDEX "feedback_created_at_idx" ON "feedback" USING btree ("created_at"); \ No newline at end of file diff --git a/apps/evi/db/migrations/meta/0000_snapshot.json b/apps/evi/db/migrations/meta/0000_snapshot.json new file mode 100644 index 00000000..0a9c1dcd --- /dev/null +++ b/apps/evi/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,110 @@ +{ + "id": "a9ba6730-0923-4225-88d4-f80553fd82ff", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.feedback": { + "name": "feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message_ref": { + "name": "message_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thread_ref": { + "name": "thread_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_ref": { + "name": "session_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "feedback_created_at_idx": { + "name": "feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/evi/db/migrations/meta/_journal.json b/apps/evi/db/migrations/meta/_journal.json index f04877e7..9e726d42 100644 --- a/apps/evi/db/migrations/meta/_journal.json +++ b/apps/evi/db/migrations/meta/_journal.json @@ -1 +1,13 @@ -{"version":"7","dialect":"postgresql","entries":[]} \ No newline at end of file +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1786476803295, + "tag": "0000_silly_pixie", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/apps/evi/db/schema.ts b/apps/evi/db/schema.ts index af3992fc..4dbc2bb5 100644 --- a/apps/evi/db/schema.ts +++ b/apps/evi/db/schema.ts @@ -1,5 +1,32 @@ // Drizzle schema for Evi's store. Tables are added by the features that need // them; the binding object exists so `drizzle(client, { schema })` in -// `agent/lib/db.ts` is typed against whatever tables exist. Kept empty while -// the database is provisioned but nothing stores in it yet. -export const schema = {} +// `agent/lib/db.ts` is typed against whatever tables exist. +import { index, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' + +/** + * Feedback about Evi's answers, captured across channels and read later by the + * weekly self-review and the evals. Two kinds of row share the table: + * a reaction (thumbs up/down, `source: 'reaction'`, no free text) and written + * feedback (`source: 'written'`, with the person's reason in `text`). `verdict` + * is the binary signal either way; `author`, `messageRef` and `[thread, session]Ref` + * place the judgement back in the conversation it came from. + */ +export const feedback = pgTable( + 'feedback', + { + id: uuid('id').primaryKey().defaultRandom(), + channel: text('channel').notNull(), + source: text('source').notNull(), + verdict: text('verdict').notNull(), + author: text('author').notNull(), + text: text('text'), + messageRef: text('message_ref'), + threadRef: text('thread_ref'), + sessionRef: text('session_ref'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + // The weekly review reads the window since last week; keep that cheap. + (table) => [index('feedback_created_at_idx').on(table.createdAt)], +) + +export const schema = { feedback } diff --git a/patches/eve@0.31.3.patch b/patches/eve@0.31.3.patch index 2ecf2117..9b4e40a6 100644 --- a/patches/eve@0.31.3.patch +++ b/patches/eve@0.31.3.patch @@ -9,11 +9,43 @@ index cdd907be4e105ae5eb277185158bf9f1105a919f..9c186d7425f167ea47af1ebd1d76adc8 \ No newline at end of file +`))}async function bridgeSend(e,t,n){let a=contextStorage.getStore()?.get(ActiveWebhookKey);if(!a)throw Error(`chatSdkChannel().send can only run during a Chat SDK webhook handler for this bridge.`);let o=serializeThread(e,n.thread,n.adapterName),s=normalizeSendInput(t),c={auth:n.auth??null,state:{thread:o}};n.callback!==void 0&&(c.callback=n.callback),n.mode!==void 0&&(c.mode=n.mode),n.title!==void 0&&(c.title=n.title);let l=a.from(o.id);return n.turnPolicy===`experimental-steer`&&hasMessageInput(t)&&await l.cancel(),l[INTERNAL_CHANNEL_DELIVER](s,c)}function hasMessageInput(e){return typeof e==`string`||Array.isArray(e)||e.message!==void 0}function initialState(){return{thread:null}}function metadataFromState(e){return{adapterName:e.thread?.adapterName??null,channelId:e.thread?.channelId??null,isDM:e.thread?.isDM??null,threadId:e.thread?.id??null}}function threadFromState(e,t){if(!t.thread)return null;try{let n=t.thread;return new ThreadImpl({adapter:e.getAdapter(n.adapterName),channelId:n.channelId,channelVisibility:n.channelVisibility,currentMessage:n.currentMessage?Message.fromJSON(n.currentMessage):void 0,id:n.id,isDM:n.isDM,stateAdapter:e.getState()})}catch(e){return log.warn(`failed to rebuild Chat SDK thread from channel state`,{error:e}),null}}function serializeReceiveTarget(e,t){if(t.thread)return t.thread;if(!t.threadId)throw Error(`chatSdkChannel().receive requires target.thread or target.threadId.`);return serializeThread(e,t.threadId,t.adapterName)}function serializeThread(e,t,n){if(typeof t==`string`){let r=n??inferAdapterName(t),i=e.getAdapter(r);return{_type:`chat:Thread`,adapterName:r,channelId:i.channelIdFromThreadId(t),channelVisibility:i.getChannelVisibility?.(t),id:t,isDM:!1}}return isSerializedThread(t)?t:t.toJSON()}function isSerializedThread(e){return`_type`in e&&e._type===`chat:Thread`}function inferAdapterName(e){let t=e.indexOf(`:`);if(t<=0)throw Error(`chatSdkChannel string thread references require options.adapterName.`);return e.slice(0,t)}function adapterNames(e){return Object.keys(e)}function routeForAdapter(e,t){return t.routes?.[e]||`${(t.route??`/eve/v1`).replace(/\/$/u,``)}/${e}`}function encodeInputAction(e,t,n){return`${e}${encodeURIComponent(t)}:${encodeURIComponent(n)}`}function decodeInputAction(e,t,n){if(!e.startsWith(t))return null;let r=e.slice(t.length),i=r.indexOf(`:`);if(i<=0)return null;try{let e=decodeURIComponent(r.slice(0,i));return{optionId:n??decodeURIComponent(r.slice(i+1)),requestId:e}}catch{return null}}function getActiveWebhook(){return contextStorage.getStore()?.get(ActiveWebhookKey)}export{getActiveWebhook,chatSdkChannel}; \ No newline at end of file +diff --git a/dist/src/public/channels/photon/photonIMessageChannel.d.ts b/dist/src/public/channels/photon/photonIMessageChannel.d.ts +index 17991b5fe9f52167c35f375e29ec0549ae3a9ab7..517e8d83c7d53cd81b001e526a96d6d90b8b102d 100644 +--- a/dist/src/public/channels/photon/photonIMessageChannel.d.ts ++++ b/dist/src/public/channels/photon/photonIMessageChannel.d.ts +@@ -16,6 +16,18 @@ export type PhotonInboundResult = { + } | null; + /** Sync or async {@link PhotonInboundResult}. */ + export type PhotonInboundResultOrPromise = PhotonInboundResult | Promise; ++/** A reaction (iMessage tapback) delivered by the Chat SDK. */ ++export interface PhotonReactionEvent { ++ /** Normalized emoji name (e.g. `thumbs_up`) or the raw emoji when unknown. */ ++ readonly emoji: string; ++ readonly rawEmoji?: string; ++ /** False for a reaction removal. */ ++ readonly added: boolean; ++ readonly messageId: string; ++ readonly threadId: string; ++ readonly userName: string; ++} ++ + /** Configuration for {@link photonIMessageChannel}. */ + export interface PhotonIMessageChannelConfig { + /** Lazy Photon project credentials, such as `connectPhotonCredentials(...)`. */ +@@ -26,6 +38,8 @@ export interface PhotonIMessageChannelConfig { + }>; + /** Inbound message policy. Defaults to dispatching every message with no user auth. */ + readonly onMessage?: (ctx: PhotonInboundMessageContext, message: Message) => PhotonInboundResultOrPromise; ++ /** Opt-in handler for inbound reactions (iMessage tapbacks). */ ++ readonly onReaction?: (reaction: PhotonReactionEvent) => void | Promise; + /** Override the default webhook route (`/eve/v1/photon`). */ + readonly route?: string; + /** Display name used by the Chat SDK runtime. Defaults to `"eve"`. */ diff --git a/dist/src/public/channels/photon/photonIMessageChannel.js b/dist/src/public/channels/photon/photonIMessageChannel.js -index 52f281b78dcf94d6fca67dd2818b4ec73b73be55..e8f8d0a698b3f84c9121e2699a998342bef89aa6 100644 +index 52f281b78dcf94d6fca67dd2818b4ec73b73be55..d40eddffee308f73fab140f997a5793be6f72560 100644 --- a/dist/src/public/channels/photon/photonIMessageChannel.js +++ b/dist/src/public/channels/photon/photonIMessageChannel.js @@ -1 +1 @@ -import{vercelOidc}from"#public/channels/auth.js";import{chatSdkChannel}from"#public/channels/chat-sdk/index.js";import{createMemoryState}from"#compiled/@chat-adapter/state-memory/index.js";import{createiMessageAdapter}from"#compiled/@photon-ai/chat-adapter-imessage/index.js";import{photonInboundContent}from"#public/channels/photon/inboundContent.js";function photonIMessageChannel(i){let a=i.webhookSecret??process.env.IMESSAGE_WEBHOOK_SECRET,o=chatSdkChannel({adapters:{imessage:createiMessageAdapter({credentials:i.credentials,...i.webhookVerifier?{webhookVerifier:i.webhookVerifier}:a?{webhookSecret:a}:{webhookVerifier:vercelOidc()}})},concurrency:`concurrent`,events:i.events,routes:{imessage:i.route??`/eve/v1/photon`},state:createMemoryState(),streaming:!1,userName:i.userName??`eve`}),s=i.onMessage??defaultOnMessage;return o.bot.onDirectMessage(async(e,t)=>{await dispatchMessage(o,s,e,t)}),o.bot.onNewMessage(/[\s\S]*/,async(e,t)=>{await dispatchMessage(o,s,e,t)}),o.channel}async function defaultOnMessage(){return{auth:null}}async function dispatchMessage(e,t,n,r){let a=await t({thread:n},r);if(a===null)return;await markReadBestEffort(e.bot.getAdapter(`imessage`),n,r);let o=photonInboundContent(r);o!==void 0&&await e.send({context:[...a.context??[]],message:o},{auth:a.auth,thread:n,turnPolicy:`experimental-steer`})}async function markReadBestEffort(e,t,n){try{await e.markRead(t.id,n.id)}catch{}}export{photonIMessageChannel}; \ No newline at end of file -+import{getActiveWebhook}from"#public/channels/chat-sdk/chatSdkChannel.js";import{vercelOidc}from"#public/channels/auth.js";import{chatSdkChannel}from"#public/channels/chat-sdk/index.js";import{createMemoryState}from"#compiled/@chat-adapter/state-memory/index.js";import{createiMessageAdapter}from"#compiled/@photon-ai/chat-adapter-imessage/index.js";import{photonInboundContent}from"#public/channels/photon/inboundContent.js";function photonIMessageChannel(i){let a=i.webhookSecret??process.env.IMESSAGE_WEBHOOK_SECRET,o=chatSdkChannel({adapters:{imessage:createiMessageAdapter({credentials:i.credentials,...i.webhookVerifier?{webhookVerifier:i.webhookVerifier}:a?{webhookSecret:a}:{webhookVerifier:vercelOidc()}})},concurrency:`concurrent`,events:i.events,routes:{imessage:i.route??`/eve/v1/photon`},state:createMemoryState(),streaming:!1,userName:i.userName??`eve`}),s=i.onMessage??defaultOnMessage;return o.bot.onDirectMessage(async(e,t)=>{await dispatchMessage(o,s,e,t)}),o.bot.onNewMessage(/[\s\S]*/,async(e,t)=>{await dispatchMessage(o,s,e,t)}),o.channel}async function defaultOnMessage(){return{auth:null}}async function dispatchMessage(e,t,n,r){let a=await t({thread:n},r);if(a===null)return;let z=photonInboundContent(r);if(typeof z===`string`&&z.trim().toLowerCase()===`!reset`){let w=getActiveWebhook();if(w?.from!==void 0){let q=await w.from(n.id).clear();await markReadBestEffort(e.bot.getAdapter(`imessage`),n,r);await n.post(q.status===`accepted`?`Session cleared. The next message starts fresh.`:`No active session here; the next message starts fresh anyway.`);return}}await markReadBestEffort(e.bot.getAdapter(`imessage`),n,r);let o=photonInboundContent(r);o!==void 0&&await e.send({context:[...a.context??[]],message:o},{auth:a.auth,thread:n,turnPolicy:`experimental-steer`})}async function markReadBestEffort(e,t,n){try{await e.markRead(t.id,n.id)}catch{}}export{photonIMessageChannel}; ++import{getActiveWebhook}from"#public/channels/chat-sdk/chatSdkChannel.js";import{vercelOidc}from"#public/channels/auth.js";import{chatSdkChannel}from"#public/channels/chat-sdk/index.js";import{createMemoryState}from"#compiled/@chat-adapter/state-memory/index.js";import{createiMessageAdapter}from"#compiled/@photon-ai/chat-adapter-imessage/index.js";import{photonInboundContent}from"#public/channels/photon/inboundContent.js";function photonIMessageChannel(i){let a=i.webhookSecret??process.env.IMESSAGE_WEBHOOK_SECRET,o=chatSdkChannel({adapters:{imessage:createiMessageAdapter({credentials:i.credentials,...i.webhookVerifier?{webhookVerifier:i.webhookVerifier}:a?{webhookSecret:a}:{webhookVerifier:vercelOidc()}})},concurrency:`concurrent`,events:i.events,routes:{imessage:i.route??`/eve/v1/photon`},state:createMemoryState(),streaming:!1,userName:i.userName??`eve`}),s=i.onMessage??defaultOnMessage;return o.bot.onDirectMessage(async(e,t)=>{await dispatchMessage(o,s,e,t)}),o.bot.onNewMessage(/[\s\S]*/,async(e,t)=>{await dispatchMessage(o,s,e,t)}),i.onReaction&&o.bot.onReaction(async e=>{try{await i.onReaction({emoji:e.emoji?.name??e.rawEmoji??``,rawEmoji:e.rawEmoji??``,added:e.added!==!1,messageId:e.messageId??``,threadId:e.threadId??``,userName:e.user?.userName??``})}catch(t){console.error(`[evi:photon] reaction callback failed`,t)}}),o.channel}async function defaultOnMessage(){return{auth:null}}async function dispatchMessage(e,t,n,r){let a=await t({thread:n},r);if(a===null)return;let z=photonInboundContent(r);if(typeof z===`string`&&z.trim().toLowerCase()===`!reset`){let w=getActiveWebhook();if(w?.from!==void 0){let q=await w.from(n.id).clear();await markReadBestEffort(e.bot.getAdapter(`imessage`),n,r);await n.post(q.status===`accepted`?`Session cleared. The next message starts fresh.`:`No active session here; the next message starts fresh anyway.`);return}}await markReadBestEffort(e.bot.getAdapter(`imessage`),n,r);let o=photonInboundContent(r);o!==void 0&&await e.send({context:[...a.context??[]],message:o},{auth:a.auth,thread:n,turnPolicy:`experimental-steer`})}async function markReadBestEffort(e,t,n){try{await e.markRead(t.id,n.id)}catch{}}export{photonIMessageChannel}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d92e8cda..17f5f2bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ overrides: '@unhead/vue': ^3.2.1 patchedDependencies: - eve@0.31.3: a523681f08db9254a2c5d3a83566aaccddbac0b1e5094d176314cb7a792b95bc + eve@0.31.3: 95d8bf38d7e09d29d37bd596b7534be228df367742d50e20e77ce2795668d171 importers: @@ -124,7 +124,7 @@ importers: dependencies: '@agent-browser/eve': specifier: ^0.33.2 - version: 0.33.2(eve@0.31.3(patch_hash=a523681f08db9254a2c5d3a83566aaccddbac0b1e5094d176314cb7a792b95bc)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0))) + version: 0.33.2(eve@0.31.3(patch_hash=95d8bf38d7e09d29d37bd596b7534be228df367742d50e20e77ce2795668d171)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0))) '@github-tools/eve-extension': specifier: ^0.3.2 version: 0.3.2(a31c2e87073339d217975897a393d465) @@ -145,7 +145,7 @@ importers: version: 2.7.0 '@vercel/connect': specifier: ^0.6.1 - version: 0.6.1(@ai-sdk/mcp@1.0.62(zod@4.4.3))(ai@7.0.51(zod@4.4.3))(better-auth@1.6.23(032c2f464d92f824949e29df2eaed1d4))(eve@0.31.3(patch_hash=a523681f08db9254a2c5d3a83566aaccddbac0b1e5094d176314cb7a792b95bc)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0))) + version: 0.6.1(@ai-sdk/mcp@1.0.62(zod@4.4.3))(ai@7.0.51(zod@4.4.3))(better-auth@1.6.23(032c2f464d92f824949e29df2eaed1d4))(eve@0.31.3(patch_hash=95d8bf38d7e09d29d37bd596b7534be228df367742d50e20e77ce2795668d171)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0))) '@vercel/oidc': specifier: ^3.8.2 version: 3.8.2 @@ -160,7 +160,7 @@ importers: version: 0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9) eve: specifier: ^0.31.3 - version: 0.31.3(patch_hash=a523681f08db9254a2c5d3a83566aaccddbac0b1e5094d176314cb7a792b95bc)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)) + version: 0.31.3(patch_hash=95d8bf38d7e09d29d37bd596b7534be228df367742d50e20e77ce2795668d171)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)) evlog: specifier: workspace:* version: link:../../packages/evlog @@ -18143,10 +18143,10 @@ snapshots: '@phc/format': 1.0.0 '@poppinss/utils': 6.10.1 - '@agent-browser/eve@0.33.2(eve@0.31.3(patch_hash=a523681f08db9254a2c5d3a83566aaccddbac0b1e5094d176314cb7a792b95bc)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)))': + '@agent-browser/eve@0.33.2(eve@0.31.3(patch_hash=95d8bf38d7e09d29d37bd596b7534be228df367742d50e20e77ce2795668d171)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)))': dependencies: '@agent-browser/sandbox': 0.33.2 - eve: 0.31.3(patch_hash=a523681f08db9254a2c5d3a83566aaccddbac0b1e5094d176314cb7a792b95bc)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)) + eve: 0.31.3(patch_hash=95d8bf38d7e09d29d37bd596b7534be228df367742d50e20e77ce2795668d171)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)) zod: 4.4.3 transitivePeerDependencies: - '@vercel/sandbox' @@ -19797,10 +19797,10 @@ snapshots: '@github-tools/eve-extension@0.3.2(a31c2e87073339d217975897a393d465)': dependencies: '@github-tools/sdk': 1.11.1(8276e1f26c356bc7c18ed70cf2379eca) - eve: 0.31.3(patch_hash=a523681f08db9254a2c5d3a83566aaccddbac0b1e5094d176314cb7a792b95bc)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)) + eve: 0.31.3(patch_hash=95d8bf38d7e09d29d37bd596b7534be228df367742d50e20e77ce2795668d171)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)) zod: 4.4.3 optionalDependencies: - '@vercel/connect': 0.6.1(@ai-sdk/mcp@1.0.62(zod@4.4.3))(ai@7.0.51(zod@4.4.3))(better-auth@1.6.23(032c2f464d92f824949e29df2eaed1d4))(eve@0.31.3(patch_hash=a523681f08db9254a2c5d3a83566aaccddbac0b1e5094d176314cb7a792b95bc)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0))) + '@vercel/connect': 0.6.1(@ai-sdk/mcp@1.0.62(zod@4.4.3))(ai@7.0.51(zod@4.4.3))(better-auth@1.6.23(032c2f464d92f824949e29df2eaed1d4))(eve@0.31.3(patch_hash=95d8bf38d7e09d29d37bd596b7534be228df367742d50e20e77ce2795668d171)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0))) transitivePeerDependencies: - '@ai-sdk/workflow' - '@workflow/ai' @@ -19813,8 +19813,8 @@ snapshots: octokit: 5.0.5 zod: 4.4.3 optionalDependencies: - '@vercel/connect': 0.6.1(@ai-sdk/mcp@1.0.62(zod@4.4.3))(ai@7.0.51(zod@4.4.3))(better-auth@1.6.23(032c2f464d92f824949e29df2eaed1d4))(eve@0.31.3(patch_hash=a523681f08db9254a2c5d3a83566aaccddbac0b1e5094d176314cb7a792b95bc)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0))) - eve: 0.31.3(patch_hash=a523681f08db9254a2c5d3a83566aaccddbac0b1e5094d176314cb7a792b95bc)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)) + '@vercel/connect': 0.6.1(@ai-sdk/mcp@1.0.62(zod@4.4.3))(ai@7.0.51(zod@4.4.3))(better-auth@1.6.23(032c2f464d92f824949e29df2eaed1d4))(eve@0.31.3(patch_hash=95d8bf38d7e09d29d37bd596b7534be228df367742d50e20e77ce2795668d171)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0))) + eve: 0.31.3(patch_hash=95d8bf38d7e09d29d37bd596b7534be228df367742d50e20e77ce2795668d171)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)) '@hono/node-server@1.19.14(hono@4.12.16)': dependencies: @@ -30318,14 +30318,14 @@ snapshots: better-auth: 1.6.23(8f830602169c0404c0bce3e45117e35b) eve: 0.30.8(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(ai@7.0.15(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(vite@8.1.4(@types/node@22.19.17)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)) - '@vercel/connect@0.6.1(@ai-sdk/mcp@1.0.62(zod@4.4.3))(ai@7.0.51(zod@4.4.3))(better-auth@1.6.23(032c2f464d92f824949e29df2eaed1d4))(eve@0.31.3(patch_hash=a523681f08db9254a2c5d3a83566aaccddbac0b1e5094d176314cb7a792b95bc)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)))': + '@vercel/connect@0.6.1(@ai-sdk/mcp@1.0.62(zod@4.4.3))(ai@7.0.51(zod@4.4.3))(better-auth@1.6.23(032c2f464d92f824949e29df2eaed1d4))(eve@0.31.3(patch_hash=95d8bf38d7e09d29d37bd596b7534be228df367742d50e20e77ce2795668d171)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)))': dependencies: '@vercel/oidc': 3.8.2 optionalDependencies: '@ai-sdk/mcp': 1.0.62(zod@4.4.3) ai: 7.0.51(zod@4.4.3) better-auth: 1.6.23(032c2f464d92f824949e29df2eaed1d4) - eve: 0.31.3(patch_hash=a523681f08db9254a2c5d3a83566aaccddbac0b1e5094d176314cb7a792b95bc)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)) + eve: 0.31.3(patch_hash=95d8bf38d7e09d29d37bd596b7534be228df367742d50e20e77ce2795668d171)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)) '@vercel/nft@1.5.0(rollup@4.60.2)': dependencies: @@ -33723,7 +33723,7 @@ snapshots: - xml2js - zephyr-agent - eve@0.31.3(patch_hash=a523681f08db9254a2c5d3a83566aaccddbac0b1e5094d176314cb7a792b95bc)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)): + eve@0.31.3(patch_hash=95d8bf38d7e09d29d37bd596b7534be228df367742d50e20e77ce2795668d171)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@vercel/blob@2.7.0)(ai@7.0.51(zod@4.4.3))(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(microsandbox@0.6.8)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0)): dependencies: ai: 7.0.51(zod@4.4.3) nitro: 3.0.260610-beta(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@vercel/blob@2.7.0)(better-sqlite3@12.11.1)(dotenv@17.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260503.1)(@electric-sql/pglite@0.5.4)(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)(bun-types@1.3.14)(kysely@0.28.17)(postgres@3.4.9))(giget@3.3.0)(jiti@2.7.0)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.2)(tsx@4.23.1)(yaml@2.9.0))