import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core"; import type { z } from "zod"; import type * as schema from "../db/schema"; /** * The Drizzle database type shared by every consumer. Both the web hub * (LibSQLDatabase over Turso) and the desktop app (BaseSQLiteDatabase over * bun:sqlite) are Drizzle databases over the same core schema; LibSQLDatabase * structurally satisfies BaseSQLiteDatabase, so capability `execute` bodies * written against the query builder run unchanged on both. */ // biome-ignore lint/suspicious/noExplicitAny: cross-driver common base (libsql + bun:sqlite) export type CapabilityDb = BaseSQLiteDatabase; /** * Optional AI narrative for a generateReport payload (OPL-2812). Produced only * when the hosted web app injects {@link CapabilityContext.generateNarrative} * and the caller sets `narrative: true`. Shape is fixed so desktop/CLI never * need to know about AI providers. */ export interface ReportNarrative { /** ~120–180 word executive summary paragraph. */ summary: string; /** One crisp sentence per project (cap 6). */ perProject: Array<{ projectName: string; sentence: string }>; } /** * Structured input handed to an injected narrative generator. Intentionally * narrow — only stats/outcomes/tickets/projects, never raw chat evidence. * Typed loosely here to avoid a circular import with the report capability; * the web implementation narrows to ReportPayloadBase. */ export type ReportNarrativeInput = { scope: string; filter: string; depth: string; report: { totalEstimatedMinutes: number; totalEvents: number; activeDays: number; projects: Array<{ projectId: string | null; projectName: string; estimatedMinutes: number; gitCommits: number; conversations: number; filesModified: number; activeDays: number; }>; outcomes?: Array<{ projectId: string | null; projectName: string; bullets: string[]; }>; ticketsByProject?: Array<{ projectId: string | null; projectName: string; tickets: Array<{ id: string; title: string | null; completed: boolean | null; }>; }>; byClientType?: { client: { estimatedMinutes: number; eventCount: number }; internal: { estimatedMinutes: number; eventCount: number }; }; range: { start: string | null; end: string | null }; }; }; /** * Everything a capability needs to run, resolved by the consumer from its own * auth model before invocation: * * - Hub chat: session → userId, getTeamDeviceFilter → organizationId/deviceIds. * - Hub MCP: OAuth access token → userId/scopes, then org resolution. * - Desktop (local): userId from local config, no org → no device scoping. */ export interface CapabilityContext { db: CapabilityDb; userId: string; /** * Active organization. When null/undefined (local desktop mode) no org * scoping is applied — the local DB is already single-tenant. */ organizationId?: string | null; /** * Device IDs owned by the org's members, from getTeamDeviceFilter. Semantics: * - `undefined` → no device scoping (local mode). * - `[]` → org member with zero devices: only events assigned to the * org's projects match (e.g. GitHub webhook events); no * device-scoped (unassigned) events. * - `[...ids]` → events on these devices OR assigned to the org's projects. */ deviceIds?: string[] | null; /** * Granted OAuth scopes (MCP consumers). Undefined for first-party session * consumers (chat), which are not scope-gated. Capabilities that mutate * should check this when present. */ scopes?: string[]; /** * Optional hosted AI narrative generator for `generateReport` (OPL-2812). * Injected by the web app when `narrative=true` and the ai-features flag is * on. Desktop/CLI leave this unset — the capability then returns the report * without `narrative` and sets `narrativeSkipped: "unsupported"`. * FAILURE = OMISSION: the generator should return null on error; the * capability never fails the report for narrative problems. */ generateNarrative?: ( payload: ReportNarrativeInput, ) => Promise; } /** A framework-neutral capability. Consumers adapt it to AI SDK `tool()`, MCP `registerTool`, or call it directly. */ export interface Capability { name: string; description: string; inputSchema: z.ZodType; /** OAuth scope required to invoke via a scope-gated consumer (MCP). Omitted = read, session-only. */ requiredScope?: string; /** Org RBAC permission a session (chat) consumer must enforce via the caller's role. Framework-neutral string; the consumer maps it to its permission matrix. Omitted = allowed for any org member. */ requiredPermission?: string; execute: (ctx: CapabilityContext, input: Input) => Promise; } /** Helper to define a capability with inferred input typing. */ export function defineCapability( cap: Capability, ): Capability { return cap; } /** * Whether a caller with the given granted OAuth scopes may invoke a capability. * A capability with no `requiredScope` is open to any authenticated caller; * otherwise the scope must be present. Single source of truth for scope gating * across every OAuth-token consumer (hub MCP, future desktop MCP) so the check * is never reimplemented per surface. */ export function isCapabilityAllowed( cap: Pick, grantedScopes: readonly string[], ): boolean { if (!cap.requiredScope) return true; return grantedScopes.includes(cap.requiredScope); } /** * Registry element type. The capability registry is heterogeneous (each * capability has its own Input/Output), so iterating it for adapters (AI SDK * `tool()`, MCP `registerTool`) needs a common element type. Individual * capabilities stay fully typed at their definition sites via * `defineCapability`; this `any` is confined to the registry/iteration seam. */ // biome-ignore lint/suspicious/noExplicitAny: heterogeneous capability registry element export type AnyCapability = Capability;