Major restructuring: - Split monolithic gatewayStore into 5 focused stores: - connectionStore: WebSocket connection and gateway lifecycle - configStore: quickConfig, workspaceInfo, MCP services - agentStore: clones, usage stats, agent management - handStore: hands, approvals, triggers, hand runs - workflowStore: workflows, workflow runs, execution - Update all components to use new stores with selector pattern - Remove
354 lines
8.3 KiB
TypeScript
354 lines
8.3 KiB
TypeScript
/**
|
|
* 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<string, unknown>;
|
|
}
|
|
|
|
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<string, string>;
|
|
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<VikingStatus> {
|
|
return this.get<VikingStatus>('/api/status');
|
|
}
|
|
|
|
async isAvailable(): Promise<boolean> {
|
|
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<void> {
|
|
await this.delete(`/api/resources`, { uri });
|
|
}
|
|
|
|
async ls(path: string): Promise<VikingEntry[]> {
|
|
const result = await this.get<{ entries: VikingEntry[] }>('/api/ls', { path });
|
|
return result.entries || [];
|
|
}
|
|
|
|
async tree(path: string, depth: number = 2): Promise<VikingTreeNode> {
|
|
return this.get<VikingTreeNode>('/api/tree', { path, depth: String(depth) });
|
|
}
|
|
|
|
// === Retrieval ===
|
|
|
|
async find(query: string, options?: FindOptions): Promise<FindResult[]> {
|
|
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<GrepResult[]> {
|
|
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<string> {
|
|
const result = await this.get<{ content: string }>('/api/read', { uri, level });
|
|
return result.content || '';
|
|
}
|
|
|
|
// === Session Management ===
|
|
|
|
async extractMemories(
|
|
sessionContent: string,
|
|
agentId?: string
|
|
): Promise<SessionExtractionResult> {
|
|
return this.post<SessionExtractionResult>('/api/session/extract', {
|
|
content: sessionContent,
|
|
agent_id: agentId,
|
|
});
|
|
}
|
|
|
|
async compactSession(
|
|
messages: Array<{ role: string; content: string }>,
|
|
): Promise<string> {
|
|
const result = await this.post<{ summary: string }>('/api/session/compact', {
|
|
messages,
|
|
});
|
|
return result.summary;
|
|
}
|
|
|
|
// === Internal HTTP Methods ===
|
|
|
|
private async get<T>(path: string, params?: Record<string, string>): Promise<T> {
|
|
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<T>(path: string, body: unknown): Promise<T> {
|
|
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<void> {
|
|
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;
|
|
}
|