Archipelago — open-source initial import
This commit is contained in:
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<object, object, unknown>
|
||||
export default component
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
registerPlugin,
|
||||
unregisterPlugin,
|
||||
getPlugin,
|
||||
getPluginsByType,
|
||||
registerRenderer,
|
||||
getRendererForContentType,
|
||||
getAllRenderers,
|
||||
} from './plugins/registry'
|
||||
import type { AIUIPlugin } from './types/plugin'
|
||||
import type { RendererDefinition } from './types/renderer'
|
||||
|
||||
function createMockPlugin(overrides: Partial<AIUIPlugin> = {}): AIUIPlugin {
|
||||
return {
|
||||
id: 'test-plugin',
|
||||
name: 'Test Plugin',
|
||||
version: '1.0.0',
|
||||
type: 'ai-provider',
|
||||
async init() {},
|
||||
async destroy() {},
|
||||
async isAvailable() { return true },
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('core exports', () => {
|
||||
it('exports plugin registry functions', () => {
|
||||
expect(registerPlugin).toBeTypeOf('function')
|
||||
expect(unregisterPlugin).toBeTypeOf('function')
|
||||
expect(getPlugin).toBeTypeOf('function')
|
||||
expect(getPluginsByType).toBeTypeOf('function')
|
||||
})
|
||||
|
||||
it('exports renderer registry functions', () => {
|
||||
expect(registerRenderer).toBeTypeOf('function')
|
||||
expect(getRendererForContentType).toBeTypeOf('function')
|
||||
expect(getAllRenderers).toBeTypeOf('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('plugin registry', () => {
|
||||
it('registers and retrieves a plugin', () => {
|
||||
const plugin = createMockPlugin({ id: 'reg-test' })
|
||||
registerPlugin(plugin)
|
||||
|
||||
const retrieved = getPlugin('reg-test')
|
||||
expect(retrieved).toBeDefined()
|
||||
expect(retrieved?.id).toBe('reg-test')
|
||||
|
||||
unregisterPlugin('reg-test')
|
||||
})
|
||||
|
||||
it('unregisters a plugin', () => {
|
||||
const plugin = createMockPlugin({ id: 'unreg-test' })
|
||||
registerPlugin(plugin)
|
||||
unregisterPlugin('unreg-test')
|
||||
|
||||
expect(getPlugin('unreg-test')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('filters plugins by type', () => {
|
||||
const p1 = createMockPlugin({ id: 'type-a', type: 'ai-provider' })
|
||||
const p2 = createMockPlugin({ id: 'type-b', type: 'storage' })
|
||||
registerPlugin(p1)
|
||||
registerPlugin(p2)
|
||||
|
||||
const providers = getPluginsByType('ai-provider')
|
||||
expect(providers.some(p => p.id === 'type-a')).toBe(true)
|
||||
expect(providers.some(p => p.id === 'type-b')).toBe(false)
|
||||
|
||||
unregisterPlugin('type-a')
|
||||
unregisterPlugin('type-b')
|
||||
})
|
||||
|
||||
it('skips duplicate registration', () => {
|
||||
const plugin = createMockPlugin({ id: 'dup-test' })
|
||||
registerPlugin(plugin)
|
||||
registerPlugin(plugin) // should warn but not throw
|
||||
|
||||
unregisterPlugin('dup-test')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './types/plugin'
|
||||
export * from './types/renderer'
|
||||
export * from './types/message'
|
||||
export * from './types/content'
|
||||
export * from './plugins/registry'
|
||||
@@ -0,0 +1,51 @@
|
||||
import { type DeepReadonly, type Ref, ref, readonly } from 'vue'
|
||||
import type { AIUIPlugin, PluginType } from '../types/plugin'
|
||||
import type { RendererDefinition } from '../types/renderer'
|
||||
|
||||
const plugins = ref<Map<string, AIUIPlugin>>(new Map())
|
||||
const renderers = ref<Map<string, RendererDefinition>>(new Map())
|
||||
|
||||
export function registerPlugin(plugin: AIUIPlugin): void {
|
||||
if (plugins.value.has(plugin.id)) {
|
||||
console.warn(`Plugin "${plugin.id}" is already registered. Skipping.`)
|
||||
return
|
||||
}
|
||||
plugins.value.set(plugin.id, plugin)
|
||||
}
|
||||
|
||||
export function unregisterPlugin(pluginId: string): void {
|
||||
plugins.value.delete(pluginId)
|
||||
}
|
||||
|
||||
export function getPlugin<T extends AIUIPlugin>(pluginId: string): T | undefined {
|
||||
return plugins.value.get(pluginId) as T | undefined
|
||||
}
|
||||
|
||||
export function getPluginsByType<T extends AIUIPlugin>(type: PluginType): T[] {
|
||||
return Array.from(plugins.value.values()).filter(
|
||||
(p) => p.type === type
|
||||
) as T[]
|
||||
}
|
||||
|
||||
export function registerRenderer(renderer: RendererDefinition): void {
|
||||
if (renderers.value.has(renderer.id)) {
|
||||
console.warn(`Renderer "${renderer.id}" is already registered. Skipping.`)
|
||||
return
|
||||
}
|
||||
renderers.value.set(renderer.id, renderer)
|
||||
}
|
||||
|
||||
export function getRendererForContentType(
|
||||
contentType: string
|
||||
): RendererDefinition | undefined {
|
||||
return Array.from(renderers.value.values()).find(
|
||||
(r) => r.contentType === contentType
|
||||
)
|
||||
}
|
||||
|
||||
export function getAllRenderers(): RendererDefinition[] {
|
||||
return Array.from(renderers.value.values())
|
||||
}
|
||||
|
||||
export const pluginRegistry: DeepReadonly<Ref<Map<string, AIUIPlugin>>> = readonly(plugins)
|
||||
export const rendererRegistry: DeepReadonly<Ref<Map<string, RendererDefinition>>> = readonly(renderers)
|
||||
@@ -0,0 +1,165 @@
|
||||
export interface ContentBlock {
|
||||
contentType: string
|
||||
data: Record<string, unknown>
|
||||
title?: string
|
||||
}
|
||||
|
||||
export interface Film {
|
||||
id: string
|
||||
title: string
|
||||
year: number
|
||||
posterUrl: string
|
||||
backdropUrl?: string
|
||||
synopsis: string
|
||||
genres: string[]
|
||||
rating: number
|
||||
runtime: number
|
||||
director: string
|
||||
cast: string[]
|
||||
trailerUrl?: string
|
||||
sources: FilmSource[]
|
||||
}
|
||||
|
||||
export interface FilmSource {
|
||||
type: 'plex' | 'nextcloud' | 'youtube' | 'free-web' | 'indeehub'
|
||||
name: string
|
||||
url: string
|
||||
quality?: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
export interface FilmRendererData {
|
||||
films: Film[]
|
||||
query?: string
|
||||
totalResults?: number
|
||||
}
|
||||
|
||||
export interface SongSource {
|
||||
type: 'plex' | 'spotify' | 'youtube' | 'apple-music' | 'bandcamp' | 'soundcloud' | 'wavlake' | 'internet_archive' | 'jamendo' | 'odysee' | 'funkwhale'
|
||||
name: string
|
||||
url: string
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export interface Song {
|
||||
id: string
|
||||
title: string
|
||||
artist: string
|
||||
album?: string
|
||||
year?: number
|
||||
coverUrl?: string
|
||||
duration?: number
|
||||
genres?: string[]
|
||||
sources?: SongSource[]
|
||||
}
|
||||
|
||||
export interface PodcastSource {
|
||||
type: 'fountain' | 'rumble' | 'youtube' | 'podcastindex' | 'castopod' | 'odysee' | 'podverse' | 'ipfs' | 'rss'
|
||||
name: string
|
||||
url: string
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export interface Podcast {
|
||||
id: string
|
||||
title: string
|
||||
host?: string
|
||||
description?: string
|
||||
coverUrl?: string
|
||||
year?: number
|
||||
episodeCount?: number
|
||||
genres?: string[]
|
||||
sources: PodcastSource[]
|
||||
}
|
||||
|
||||
export interface PodcastRendererData {
|
||||
podcasts: Podcast[]
|
||||
query?: string
|
||||
totalResults?: number
|
||||
}
|
||||
|
||||
export interface TVSeries {
|
||||
id: string
|
||||
title: string
|
||||
year?: number
|
||||
endYear?: number
|
||||
posterUrl?: string
|
||||
backdropUrl?: string
|
||||
synopsis?: string
|
||||
genres?: string[]
|
||||
rating?: number
|
||||
seasons?: number
|
||||
episodes?: number
|
||||
status?: 'ongoing' | 'ended' | 'cancelled' | 'upcoming'
|
||||
network?: string
|
||||
creator?: string
|
||||
cast?: string[]
|
||||
sources?: TVSeriesSource[]
|
||||
}
|
||||
|
||||
export interface TVSeriesSource {
|
||||
type: 'plex' | 'nextcloud' | 'youtube' | 'netflix' | 'free-web' | 'local'
|
||||
name: string
|
||||
url: string
|
||||
quality?: string
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export interface Book {
|
||||
id: string
|
||||
title: string
|
||||
author: string
|
||||
year?: number
|
||||
coverUrl?: string
|
||||
description?: string
|
||||
genres?: string[]
|
||||
pages?: number
|
||||
isbn?: string
|
||||
rating?: number
|
||||
sources?: BookSource[]
|
||||
}
|
||||
|
||||
export interface BookSource {
|
||||
type: 'openlibrary' | 'gutenberg' | 'archive' | 'goodreads' | 'libgen' | 'local'
|
||||
name: string
|
||||
url: string
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export interface ImageItem {
|
||||
id: string
|
||||
url: string
|
||||
title?: string
|
||||
description?: string
|
||||
alt?: string
|
||||
width?: number
|
||||
height?: number
|
||||
source?: string
|
||||
attribution?: string
|
||||
}
|
||||
|
||||
export interface Place {
|
||||
id: string
|
||||
name: string
|
||||
address?: string
|
||||
city?: string
|
||||
cuisine?: string
|
||||
category?: string
|
||||
rating?: number
|
||||
priceLevel?: number // 1-4 ($-$$$$)
|
||||
phone?: string
|
||||
website?: string
|
||||
hours?: string
|
||||
description?: string
|
||||
photoUrl?: string
|
||||
lat?: number
|
||||
lng?: number
|
||||
sources?: PlaceSource[]
|
||||
}
|
||||
|
||||
export interface PlaceSource {
|
||||
type: 'gmaps' | 'osm' | 'yelp' | 'tripadvisor' | 'foursquare' | 'local'
|
||||
name: string
|
||||
url: string
|
||||
icon?: string
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { ContentBlock } from './content'
|
||||
|
||||
export interface WebSearchResult {
|
||||
title: string
|
||||
url: string
|
||||
content?: string
|
||||
/** Image/thumbnail URL from search engine (e.g. SearXNG img_src) */
|
||||
imgSrc?: string
|
||||
}
|
||||
|
||||
export interface ImageAttachment {
|
||||
/** Base64-encoded image data (no data: prefix) */
|
||||
data: string
|
||||
/** MIME type e.g. image/jpeg, image/png, image/gif, image/webp */
|
||||
mediaType: string
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: string
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content: string
|
||||
contentBlocks?: ContentBlock[]
|
||||
timestamp: number
|
||||
model?: string
|
||||
usage?: { promptTokens: number; completionTokens: number }
|
||||
replyTo?: string
|
||||
reactions?: Reaction[]
|
||||
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error'
|
||||
/** Web search results (articles, links) attached when query used web search */
|
||||
webResults?: WebSearchResult[]
|
||||
/** Timestamp of last edit (set when user edits a sent message) */
|
||||
editedAt?: number
|
||||
/** Attached images (base64, max 4) for vision-capable models */
|
||||
images?: ImageAttachment[]
|
||||
/** User feedback: 'up' = thumbs up, 'down' = thumbs down */
|
||||
feedback?: 'up' | 'down'
|
||||
}
|
||||
|
||||
export interface Reaction {
|
||||
emoji: string
|
||||
userId: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface Conversation {
|
||||
id: string
|
||||
title: string
|
||||
messages: Message[]
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
model?: string
|
||||
systemPrompt?: string
|
||||
/** ID of parent conversation this was branched from */
|
||||
parentConversationId?: string
|
||||
/** Message ID in parent where this branch forked */
|
||||
branchPoint?: string
|
||||
/** IDs of child branches forked from this conversation */
|
||||
childBranchIds?: string[]
|
||||
/** ID of the persona applied to this conversation */
|
||||
personaId?: string
|
||||
/** Generation params (persisted per conversation) */
|
||||
temperature?: number
|
||||
maxTokens?: number
|
||||
topP?: number
|
||||
/** Comma-separated stop sequences */
|
||||
stopSequences?: string[]
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
export type PluginType =
|
||||
| 'ai-provider'
|
||||
| 'media-source'
|
||||
| 'messaging'
|
||||
| 'storage'
|
||||
| 'renderer'
|
||||
| 'file-handler'
|
||||
| 'crypto'
|
||||
| 'search'
|
||||
| 'auth'
|
||||
| 'wallet'
|
||||
| 'social-embed'
|
||||
| 'mcp'
|
||||
| 'media'
|
||||
|
||||
export interface PluginContext {
|
||||
settings: PluginSettingsStore
|
||||
events: PluginEventBus
|
||||
logger: PluginLogger
|
||||
}
|
||||
|
||||
export interface PluginSettingsStore {
|
||||
get<T>(key: string): T | undefined
|
||||
set<T>(key: string, value: T): void
|
||||
}
|
||||
|
||||
export interface PluginEventBus {
|
||||
emit(event: string, payload?: unknown): void
|
||||
on(event: string, handler: (payload?: unknown) => void): () => void
|
||||
}
|
||||
|
||||
export interface PluginLogger {
|
||||
info(message: string, ...args: unknown[]): void
|
||||
warn(message: string, ...args: unknown[]): void
|
||||
error(message: string, ...args: unknown[]): void
|
||||
}
|
||||
|
||||
export interface AIUIPlugin {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
type: PluginType
|
||||
description?: string
|
||||
icon?: string
|
||||
init(context: PluginContext): Promise<void>
|
||||
destroy(): Promise<void>
|
||||
isAvailable(): Promise<boolean>
|
||||
}
|
||||
|
||||
export interface AIProviderAdapter extends AIUIPlugin {
|
||||
type: 'ai-provider'
|
||||
chat(messages: ChatMessage[], options: ChatOptions): AsyncIterable<ChatChunk>
|
||||
models(): Promise<AIModel[]>
|
||||
supportsStreaming: boolean
|
||||
supportsVision: boolean
|
||||
supportsTools: boolean
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'system' | 'user' | 'assistant' | 'tool'
|
||||
content: string | ContentPart[]
|
||||
toolCalls?: ToolCall[]
|
||||
toolCallId?: string
|
||||
}
|
||||
|
||||
export interface ContentPart {
|
||||
type: 'text' | 'image_url'
|
||||
text?: string
|
||||
imageUrl?: string
|
||||
}
|
||||
|
||||
export interface ChatOptions {
|
||||
model: string
|
||||
temperature?: number
|
||||
maxTokens?: number
|
||||
tools?: ToolDefinition[]
|
||||
stream?: boolean
|
||||
}
|
||||
|
||||
export interface ChatChunk {
|
||||
type: 'text' | 'tool_call' | 'done' | 'error'
|
||||
text?: string
|
||||
toolCall?: ToolCall
|
||||
error?: string
|
||||
usage?: { promptTokens: number; completionTokens: number }
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
id: string
|
||||
name: string
|
||||
arguments: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface ToolDefinition {
|
||||
name: string
|
||||
description: string
|
||||
parameters: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface ToolResult {
|
||||
toolCallId: string
|
||||
content: string
|
||||
isError: boolean
|
||||
}
|
||||
|
||||
export interface AIModel {
|
||||
id: string
|
||||
name: string
|
||||
provider: string
|
||||
supportsVision: boolean
|
||||
supportsTools: boolean
|
||||
contextWindow: number
|
||||
}
|
||||
|
||||
export interface MediaSourcePlugin extends AIUIPlugin {
|
||||
type: 'media-source'
|
||||
search(query: string): Promise<MediaItem[]>
|
||||
getLibrary(filters?: Record<string, unknown>): Promise<MediaItem[]>
|
||||
getPlayUrl(itemId: string): Promise<string>
|
||||
}
|
||||
|
||||
export interface MediaItem {
|
||||
id: string
|
||||
title: string
|
||||
type: 'film' | 'tv' | 'music' | 'podcast' | 'audiobook'
|
||||
posterUrl?: string
|
||||
year?: number
|
||||
rating?: number
|
||||
source: string
|
||||
sourceIcon?: string
|
||||
}
|
||||
|
||||
export interface WalletPlugin extends AIUIPlugin {
|
||||
type: 'wallet'
|
||||
supports: PaymentMethod[]
|
||||
isInstalled(): Promise<boolean>
|
||||
getPayUri(request: PaymentRequest): string
|
||||
openWallet(request: PaymentRequest): Promise<void>
|
||||
}
|
||||
|
||||
export type PaymentMethod = 'lightning' | 'onchain' | 'cashu' | 'fedimint'
|
||||
|
||||
export interface PaymentRequest {
|
||||
type: PaymentMethod
|
||||
invoice?: string
|
||||
address?: string
|
||||
amount?: number
|
||||
memo?: string
|
||||
lnurl?: string
|
||||
cashuToken?: string
|
||||
mintUrl?: string
|
||||
}
|
||||
|
||||
export interface SocialEmbedPlugin extends AIUIPlugin {
|
||||
type: 'social-embed'
|
||||
platform: 'x' | 'nostr' | 'mastodon' | 'bluesky'
|
||||
fetchPost(url: string): Promise<SocialPost>
|
||||
fetchThread(url: string): Promise<SocialPost[]>
|
||||
}
|
||||
|
||||
export interface SocialPost {
|
||||
id: string
|
||||
author: { name: string; handle: string; avatarUrl: string }
|
||||
content: string
|
||||
media?: { type: 'image' | 'video'; url: string }[]
|
||||
metrics?: { likes: number; reposts: number; replies: number }
|
||||
timestamp: string
|
||||
url: string
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Component } from 'vue'
|
||||
|
||||
export type SurfaceType =
|
||||
| 'chat-preview'
|
||||
| 'chat-play'
|
||||
| 'panel-preview'
|
||||
| 'panel-play'
|
||||
| 'panel-edit'
|
||||
|
||||
export interface RendererDefinition {
|
||||
id: string
|
||||
name: string
|
||||
contentType: string
|
||||
surfaces: SurfaceType[]
|
||||
chatPreview?: Component | (() => Promise<Component>)
|
||||
chatPlay?: Component | (() => Promise<Component>)
|
||||
panelPreview?: Component | (() => Promise<Component>)
|
||||
panelPlay?: Component | (() => Promise<Component>)
|
||||
panelEdit?: Component | (() => Promise<Component>)
|
||||
}
|
||||
Reference in New Issue
Block a user