129 lines
3.7 KiB
TypeScript
129 lines
3.7 KiB
TypeScript
import { z } from 'zod'
|
|
import type { Connection } from '#/features/connections/model'
|
|
import { mastodonSourceSchema } from '#/features/platforms/mastodon-source'
|
|
import { InputError } from '#/features/posts/inputs'
|
|
import { columnSchema, deckSchema, MAX_COLUMNS, newId } from './model'
|
|
|
|
const toolColumn = columnSchema.omit({ id: true, source: true }).extend({
|
|
id: columnSchema.shape.id
|
|
.optional()
|
|
.describe(
|
|
'Keep an existing column ID when editing; omit for a new column.',
|
|
),
|
|
source: z.union([
|
|
mastodonSourceSchema,
|
|
z.discriminatedUnion('kind', [
|
|
z
|
|
.object({
|
|
platform: z.literal('twitter').default('twitter'),
|
|
kind: z.literal('search'),
|
|
query: z.string().trim().min(1).max(512),
|
|
product: z.enum(['Latest', 'Top']).default('Latest'),
|
|
following: z.boolean().default(false),
|
|
})
|
|
.strict(),
|
|
z
|
|
.object({
|
|
platform: z.literal('twitter').default('twitter'),
|
|
kind: z.literal('user'),
|
|
target: z.string().min(1).max(256),
|
|
})
|
|
.strict(),
|
|
z
|
|
.object({
|
|
platform: z.literal('twitter').default('twitter'),
|
|
kind: z.literal('list'),
|
|
target: z.string().min(1).max(256),
|
|
})
|
|
.strict(),
|
|
]),
|
|
]),
|
|
})
|
|
export const emptyToolInput = z.object({}).strict()
|
|
export const deckIdInput = z.object({ deckId: deckSchema.shape.id }).strict()
|
|
export const deleteDeckToolInput = deckIdInput
|
|
.extend({ expectedRevision: z.number().int().positive().optional() })
|
|
.strict()
|
|
export const getDeckInput = z
|
|
.object({ deckId: deckSchema.shape.id.optional() })
|
|
.strict()
|
|
export const setDeckInput = z
|
|
.object({
|
|
deckId: deckSchema.shape.id
|
|
.optional()
|
|
.describe(
|
|
'Omit to create a temporary view in this tab; supply an existing ID to replace and activate it.',
|
|
),
|
|
expectedRevision: z
|
|
.number()
|
|
.int()
|
|
.positive()
|
|
.optional()
|
|
.describe(
|
|
'Required when replacing a saved deck; omit for a temporary view.',
|
|
),
|
|
title: deckSchema.shape.title,
|
|
columns: z
|
|
.array(toolColumn)
|
|
.max(MAX_COLUMNS)
|
|
.describe(
|
|
'Complete ordered columns, each bound to a connection ID from list_connections. Omitted existing columns are removed.',
|
|
),
|
|
})
|
|
.strict()
|
|
|
|
export function prepareDeck(
|
|
input: unknown,
|
|
connections: readonly Connection[],
|
|
) {
|
|
const parsed = setDeckInput.parse(input)
|
|
const deck = deckSchema.parse({
|
|
id: parsed.deckId ?? newId(),
|
|
title: parsed.title,
|
|
columns: parsed.columns.map((column) => ({
|
|
...column,
|
|
id: column.id ?? newId(),
|
|
})),
|
|
})
|
|
for (const column of deck.columns) {
|
|
if (
|
|
!connections.some(
|
|
(connection) =>
|
|
connection.id === column.connectionId &&
|
|
connection.platform === column.source.platform &&
|
|
connection.status === 'connected',
|
|
)
|
|
)
|
|
throw new InputError(`Unavailable connection: ${column.connectionId}`)
|
|
}
|
|
return deck
|
|
}
|
|
|
|
export async function toolResult(run: () => unknown | Promise<unknown>) {
|
|
try {
|
|
return {
|
|
content: [
|
|
{ type: 'text' as const, text: JSON.stringify(await run()) },
|
|
] as const,
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
isError: true,
|
|
content: [
|
|
{
|
|
type: 'text' as const,
|
|
text: JSON.stringify({
|
|
code:
|
|
error instanceof z.ZodError || error instanceof InputError
|
|
? 'invalid-input'
|
|
: 'tool-error',
|
|
message:
|
|
error instanceof Error ? error.message : 'Tool execution failed.',
|
|
retryable: false,
|
|
}),
|
|
},
|
|
] as const,
|
|
}
|
|
}
|
|
}
|