feat(memora): add Memora adapter with read-only SQLite backend

This commit is contained in:
Hibryda 2026-03-11 04:09:29 +01:00
parent ad7e24e40d
commit f3f740a8fe
4 changed files with 500 additions and 0 deletions

View file

@ -9,6 +9,8 @@
import { CLAUDE_PROVIDER } from './lib/providers/claude';
import { CODEX_PROVIDER } from './lib/providers/codex';
import { OLLAMA_PROVIDER } from './lib/providers/ollama';
import { registerMemoryAdapter } from './lib/adapters/memory-adapter';
import { MemoraAdapter } from './lib/adapters/memora-bridge';
import { loadWorkspace, getActiveTab, setActiveTab, setActiveProject, getEnabledProjects } from './lib/stores/workspace.svelte';
// Workspace components
@ -72,6 +74,9 @@
registerProvider(CLAUDE_PROVIDER);
registerProvider(CODEX_PROVIDER);
registerProvider(OLLAMA_PROVIDER);
const memora = new MemoraAdapter();
registerMemoryAdapter(memora);
memora.checkAvailability();
startAgentDispatcher();
startHealthTick();

View file

@ -0,0 +1,122 @@
/**
* Memora IPC bridge read-only access to the Memora memory database.
* Wraps Tauri commands and provides a MemoryAdapter implementation.
*/
import { invoke } from '@tauri-apps/api/core';
import type { MemoryAdapter, MemoryNode, MemorySearchResult } from './memory-adapter';
// --- Raw IPC types (match Rust structs) ---
interface MemoraNode {
id: number;
content: string;
tags: string[];
metadata?: Record<string, unknown>;
created_at?: string;
updated_at?: string;
}
interface MemoraSearchResult {
nodes: MemoraNode[];
total: number;
}
// --- IPC wrappers ---
export async function memoraAvailable(): Promise<boolean> {
return invoke<boolean>('memora_available');
}
export async function memoraList(options?: {
tags?: string[];
limit?: number;
offset?: number;
}): Promise<MemoraSearchResult> {
return invoke<MemoraSearchResult>('memora_list', {
tags: options?.tags ?? null,
limit: options?.limit ?? 50,
offset: options?.offset ?? 0,
});
}
export async function memoraSearch(
query: string,
options?: { tags?: string[]; limit?: number },
): Promise<MemoraSearchResult> {
return invoke<MemoraSearchResult>('memora_search', {
query,
tags: options?.tags ?? null,
limit: options?.limit ?? 50,
});
}
export async function memoraGet(id: number): Promise<MemoraNode | null> {
return invoke<MemoraNode | null>('memora_get', { id });
}
// --- MemoryAdapter implementation ---
function toMemoryNode(n: MemoraNode): MemoryNode {
return {
id: n.id,
content: n.content,
tags: n.tags,
metadata: n.metadata,
created_at: n.created_at,
updated_at: n.updated_at,
};
}
function toSearchResult(r: MemoraSearchResult): MemorySearchResult {
return {
nodes: r.nodes.map(toMemoryNode),
total: r.total,
};
}
export class MemoraAdapter implements MemoryAdapter {
readonly name = 'memora';
private _available: boolean | null = null;
get available(): boolean {
// Optimistic: assume available until first check proves otherwise.
// Actual availability is checked lazily on first operation.
return this._available ?? true;
}
async checkAvailability(): Promise<boolean> {
this._available = await memoraAvailable();
return this._available;
}
async list(options?: {
tags?: string[];
limit?: number;
offset?: number;
}): Promise<MemorySearchResult> {
const result = await memoraList(options);
this._available = true;
return toSearchResult(result);
}
async search(
query: string,
options?: { tags?: string[]; limit?: number },
): Promise<MemorySearchResult> {
const result = await memoraSearch(query, options);
this._available = true;
return toSearchResult(result);
}
async get(id: string | number): Promise<MemoryNode | null> {
const numId = typeof id === 'string' ? parseInt(id, 10) : id;
if (isNaN(numId)) return null;
const node = await memoraGet(numId);
if (node) {
this._available = true;
return toMemoryNode(node);
}
return null;
}
}