/** * OpenViking HTTP API Client * * TypeScript client for communicating with the OpenViking Server. * OpenViking is an open-source context database for AI agents by Volcengine. * * API Reference: https://github.com/volcengine/OpenViking * Default server port: 1933 */ // === Types === export interface VikingStatus { status: 'ok' | 'error'; version?: string; uptime?: number; workspace?: string; } export interface VikingEntry { uri: string; name: string; type: 'file' | 'directory'; size?: number; modifiedAt?: string; abstract?: string; } export interface VikingTreeNode { uri: string; name: string; type: 'file' | 'directory'; children?: VikingTreeNode[]; } export type ContextLevel = 'L0' | 'L1' | 'L2'; export interface FindOptions { scope?: string; level?: ContextLevel; limit?: number; minScore?: number; } export interface FindResult { uri: string; score: number; content: string; level: ContextLevel; abstract?: string; overview?: string; metadata?: Record; } export interface GrepOptions { uri?: string; caseSensitive?: boolean; limit?: number; } export interface GrepResult { uri: string; line: number; content: string; matchStart: number; matchEnd: number; } export interface AddResourceOptions { metadata?: Record; wait?: boolean; } export interface ExtractedMemory { category: 'user_preference' | 'user_fact' | 'agent_lesson' | 'agent_pattern' | 'task'; content: string; tags: string[]; importance: number; suggestedUri: string; } export interface SessionExtractionResult { memories: ExtractedMemory[]; summary: string; tokensSaved?: number; } export interface RetrievalTraceStep { uri: string; score: number; action: 'entered' | 'skipped' | 'matched'; level: ContextLevel; childrenExplored?: number; } export interface RetrievalTrace { query: string; steps: RetrievalTraceStep[]; totalTokensUsed: number; tokensByLevel: { L0: number; L1: number; L2: number }; duration: number; } // === Client Implementation === export class VikingHttpClient { private baseUrl: string; private timeout: number; constructor(baseUrl: string = 'http://localhost:1933', timeout: number = 30000) { this.baseUrl = baseUrl.replace(/\/$/, ''); this.timeout = timeout; } // === Health & Status === async status(): Promise { return this.get('/api/status'); } async isAvailable(): Promise { try { const result = await this.status(); return result.status === 'ok'; } catch { return false; } } // === Resource Management === async addResource( uri: string, content: string, options?: AddResourceOptions ): Promise<{ uri: string; status: string }> { return this.post('/api/resources', { uri, content, metadata: options?.metadata, wait: options?.wait ?? false, }); } async removeResource(uri: string): Promise { await this.delete(`/api/resources`, { uri }); } async ls(path: string): Promise { const result = await this.get<{ entries: VikingEntry[] }>('/api/ls', { path }); return result.entries || []; } async tree(path: string, depth: number = 2): Promise { return this.get('/api/tree', { path, depth: String(depth) }); } // === Retrieval === async find(query: string, options?: FindOptions): Promise { const result = await this.post<{ results: FindResult[]; trace?: RetrievalTrace }>( '/api/find', { query, scope: options?.scope, level: options?.level || 'L1', limit: options?.limit || 10, min_score: options?.minScore, } ); return result.results || []; } async findWithTrace( query: string, options?: FindOptions ): Promise<{ results: FindResult[]; trace: RetrievalTrace }> { return this.post('/api/find', { query, scope: options?.scope, level: options?.level || 'L1', limit: options?.limit || 10, min_score: options?.minScore, include_trace: true, }); } async grep( pattern: string, options?: GrepOptions ): Promise { const result = await this.post<{ results: GrepResult[] }>('/api/grep', { pattern, uri: options?.uri, case_sensitive: options?.caseSensitive ?? false, limit: options?.limit || 20, }); return result.results || []; } // === Memory Operations === async readContent(uri: string, level: ContextLevel = 'L1'): Promise { const result = await this.get<{ content: string }>('/api/read', { uri, level }); return result.content || ''; } // === Session Management === async extractMemories( sessionContent: string, agentId?: string ): Promise { return this.post('/api/session/extract', { content: sessionContent, agent_id: agentId, }); } async compactSession( messages: Array<{ role: string; content: string }>, ): Promise { const result = await this.post<{ summary: string }>('/api/session/compact', { messages, }); return result.summary; } // === Internal HTTP Methods === private async get(path: string, params?: Record): Promise { const url = new URL(`${this.baseUrl}${path}`); if (params) { for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null) { url.searchParams.set(key, value); } } } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(url.toString(), { method: 'GET', headers: { 'Accept': 'application/json' }, signal: controller.signal, }); if (!response.ok) { throw new VikingError( `Viking API error: ${response.status} ${response.statusText}`, response.status ); } return await response.json() as T; } finally { clearTimeout(timeoutId); } } private async post(path: string, body: unknown): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(`${this.baseUrl}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify(body), signal: controller.signal, }); if (!response.ok) { const errorBody = await response.text().catch(() => ''); throw new VikingError( `Viking API error: ${response.status} ${response.statusText} - ${errorBody}`, response.status ); } return await response.json() as T; } finally { clearTimeout(timeoutId); } } private async delete(path: string, body?: unknown): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(`${this.baseUrl}${path}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: body ? JSON.stringify(body) : undefined, signal: controller.signal, }); if (!response.ok) { throw new VikingError( `Viking API error: ${response.status} ${response.statusText}`, response.status ); } } finally { clearTimeout(timeoutId); } } } // === Error Class === export class VikingError extends Error { constructor( message: string, public readonly statusCode?: number ) { super(message); this.name = 'VikingError'; } } // === Singleton === let _instance: VikingHttpClient | null = null; /** * Get the singleton VikingHttpClient instance. * Uses default configuration (localhost:1933). */ export function getVikingClient(baseUrl?: string): VikingHttpClient { if (!_instance) { _instance = new VikingHttpClient(baseUrl); } return _instance; } /** * Reset the singleton instance. * Useful for testing or reconfiguration. */ export function resetVikingClient(): void { _instance = null; }