Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,647 @@
/**
* @ruvector/ospipe - RuVector-enhanced personal AI memory SDK
*
* Extends @screenpipe/js with semantic vector search, knowledge graphs,
* temporal queries, and AI safety features powered by the RuVector ecosystem.
*
* @packageDocumentation
*/
// ---- Types ----
/** Configuration options for the OsPipe client. */
export interface OsPipeConfig {
/** OSpipe REST API base URL (default: http://localhost:3030) */
baseUrl?: string;
/** API version (default: "v2") */
apiVersion?: "v1" | "v2";
/** Default number of results (default: 10) */
defaultK?: number;
/** Semantic weight for hybrid search 0-1 (default: 0.7) */
hybridWeight?: number;
/** Enable MMR deduplication (default: true) */
rerank?: boolean;
/** Request timeout in milliseconds (default: 10000) */
timeout?: number;
/** Maximum retries for failed requests (default: 3) */
maxRetries?: number;
}
/** Options for semantic vector search queries. */
export interface SemanticSearchOptions {
/** Number of results to return */
k?: number;
/** Distance metric */
metric?: "cosine" | "euclidean" | "dot";
/** Metadata filters */
filters?: SearchFilters;
/** Enable MMR deduplication */
rerank?: boolean;
/** Include confidence bounds */
confidence?: boolean;
}
/** Filters to narrow search results by metadata. */
export interface SearchFilters {
/** Filter by application name */
app?: string;
/** Filter by window title */
window?: string;
/** Filter by content type */
contentType?: "screen" | "audio" | "ui" | "all";
/** Filter by time range (ISO 8601 strings) */
timeRange?: { start: string; end: string };
/** Filter by monitor index */
monitor?: number;
/** Filter by speaker name (audio content) */
speaker?: string;
/** Filter by language code */
language?: string;
}
/** A single search result from a semantic or keyword query. */
export interface SearchResult {
/** Unique identifier for the content chunk */
id: string;
/** Relevance score (higher is more relevant) */
score: number;
/** The matched content text */
content: string;
/** Source type of the content */
source: "screen" | "audio" | "ui";
/** ISO 8601 timestamp when the content was captured */
timestamp: string;
/** Additional metadata about the content */
metadata: {
app?: string;
window?: string;
monitor?: number;
speaker?: string;
confidence?: number;
language?: string;
};
}
/** Result of a knowledge graph query. */
export interface GraphResult {
/** Nodes in the result subgraph */
nodes: GraphNode[];
/** Edges connecting the nodes */
edges: GraphEdge[];
}
/** A node in the knowledge graph. */
export interface GraphNode {
/** Unique node identifier */
id: string;
/** Human-readable label */
label: string;
/** Node type category */
type: "App" | "Window" | "Person" | "Topic" | "Meeting" | "Symbol";
/** Arbitrary key-value properties */
properties: Record<string, unknown>;
}
/** An edge in the knowledge graph connecting two nodes. */
export interface GraphEdge {
/** Source node ID */
source: string;
/** Target node ID */
target: string;
/** Relationship type */
type: string;
/** Arbitrary key-value properties */
properties: Record<string, unknown>;
}
/** A temporal delta result showing changes over time. */
export interface DeltaResult {
/** ISO 8601 timestamp of the delta snapshot */
timestamp: string;
/** Application where the change occurred */
app: string;
/** List of individual changes */
changes: DeltaChange[];
}
/** A single positional change within a delta result. */
export interface DeltaChange {
/** Character position where the change occurred */
position: number;
/** Text that was removed */
removed: string;
/** Text that was added */
added: string;
}
/** Options for temporal delta queries. */
export interface DeltaQueryOptions {
/** Filter by application name */
app?: string;
/** Filter by file path */
file?: string;
/** Time range for the delta query (ISO 8601 strings) */
timeRange: { start: string; end: string };
/** Include full change details (default: false) */
includeChanges?: boolean;
}
/** An attention-weighted event from the real-time stream. */
export interface AttentionEvent {
/** Category of the attention event */
category:
| "code_change"
| "person_mention"
| "topic_shift"
| "context_switch"
| "meeting_start"
| "meeting_end";
/** Attention score 0-1 (higher = more important) */
attention: number;
/** Human-readable summary of the event */
summary: string;
/** ISO 8601 timestamp of the event */
timestamp: string;
/** The underlying search result that triggered the event */
source: SearchResult;
}
/** Pipeline statistics from the OsPipe server. */
export interface PipelineStats {
/** Total number of ingested content chunks */
totalIngested: number;
/** Total number of deduplicated (skipped) chunks */
totalDeduplicated: number;
/** Total number of denied (safety filtered) chunks */
totalDenied: number;
/** Total storage used in bytes */
storageBytes: number;
/** Number of entries in the vector index */
indexSize: number;
/** Server uptime in seconds */
uptime: number;
}
/** The resolved query route type. */
export type QueryRoute = "semantic" | "keyword" | "graph" | "temporal" | "hybrid";
// ---- Client ----
/**
* OsPipe client for interacting with the RuVector-enhanced personal AI memory system.
*
* Provides semantic vector search, knowledge graph queries, temporal delta queries,
* attention-weighted streaming, and backward-compatible Screenpipe API access.
*
* @example
* ```typescript
* import { OsPipe } from "@ruvector/ospipe";
*
* const client = new OsPipe({ baseUrl: "http://localhost:3030" });
*
* // Semantic search
* const results = await client.queryRuVector("authentication flow");
*
* // Knowledge graph
* const graph = await client.queryGraph("MATCH (a:App)-[:USED_BY]->(p:Person) RETURN a, p");
*
* // Temporal deltas
* const deltas = await client.queryDelta({
* timeRange: { start: "2026-02-12T00:00:00Z", end: "2026-02-12T23:59:59Z" },
* app: "VSCode",
* });
* ```
*/
export class OsPipe {
private baseUrl: string;
private apiVersion: string;
private defaultK: number;
private hybridWeight: number;
private rerank: boolean;
private timeout: number;
private maxRetries: number;
constructor(config: OsPipeConfig = {}) {
this.baseUrl = config.baseUrl ?? "http://localhost:3030";
this.apiVersion = config.apiVersion ?? "v2";
this.defaultK = config.defaultK ?? 10;
this.hybridWeight = config.hybridWeight ?? 0.7;
this.rerank = config.rerank ?? true;
this.timeout = config.timeout ?? 10_000;
this.maxRetries = config.maxRetries ?? 3;
}
// ---- Internal Helpers ----
/**
* Fetch with exponential backoff retry and per-request timeout.
*
* Retries are only attempted for network errors and HTTP 5xx responses.
* Client errors (4xx) are never retried.
*
* @param url - Request URL
* @param options - Standard RequestInit options
* @param retries - Maximum number of retry attempts (default: this.maxRetries)
* @param backoffMs - Initial backoff delay in milliseconds (default: 300)
* @returns The fetch Response
* @throws {Error} After all retries are exhausted or on a non-retryable error
*/
private async fetchWithRetry(
url: string,
options?: RequestInit,
retries?: number,
backoffMs = 300,
): Promise<Response> {
const maxAttempts = retries ?? this.maxRetries;
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
// Merge the timeout signal with any caller-provided signal.
const callerSignal = options?.signal;
if (callerSignal?.aborted) {
clearTimeout(timeoutId);
throw new DOMException("The operation was aborted.", "AbortError");
}
// If the caller provided a signal, listen for its abort to propagate.
const onCallerAbort = () => controller.abort();
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
// Do not retry client errors (4xx).
if (response.status >= 400 && response.status < 500) {
return response;
}
// Retry on server errors (5xx).
if (response.status >= 500 && attempt < maxAttempts) {
await this.sleep(backoffMs * Math.pow(2, attempt));
continue;
}
return response;
} catch (error: unknown) {
// If the caller aborted, propagate immediately without retry.
if (callerSignal?.aborted) {
throw error;
}
// If this was the last attempt, throw.
if (attempt >= maxAttempts) {
throw error;
}
// Retry on network / timeout errors.
await this.sleep(backoffMs * Math.pow(2, attempt));
} finally {
clearTimeout(timeoutId);
callerSignal?.removeEventListener("abort", onCallerAbort);
}
}
// Unreachable, but satisfies the type checker.
throw new Error("fetchWithRetry: unexpected exit");
}
/** Sleep helper for backoff delays. */
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// ---- Semantic Vector Search ----
/**
* Perform a semantic vector search across all ingested content.
*
* Uses RuVector HNSW index for approximate nearest neighbor search
* with optional MMR deduplication and metadata filtering.
*
* @param query - Natural language query string
* @param options - Search configuration options
* @returns Array of search results ranked by relevance
* @throws {Error} If the search request fails
*
* @example
* ```typescript
* const results = await client.queryRuVector("user login issues", {
* k: 5,
* filters: { app: "Chrome", contentType: "screen" },
* rerank: true,
* });
* ```
*/
async queryRuVector(
query: string,
options: SemanticSearchOptions = {}
): Promise<SearchResult[]> {
const k = options.k ?? this.defaultK;
const response = await this.fetchWithRetry(
`${this.baseUrl}/${this.apiVersion}/search`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query,
mode: "semantic",
k,
metric: options.metric ?? "cosine",
filters: options.filters,
rerank: options.rerank ?? this.rerank,
confidence: options.confidence ?? false,
}),
},
);
if (!response.ok) {
throw new Error(`Search failed: ${response.statusText}`);
}
return (await response.json()) as SearchResult[];
}
// ---- Knowledge Graph Query ----
/**
* Query the knowledge graph using a Cypher-like query language.
*
* The knowledge graph connects apps, windows, people, topics, meetings,
* and code symbols with typed relationships extracted from captured content.
*
* @param cypher - Cypher query string
* @returns Graph result containing matched nodes and edges
* @throws {Error} If the graph query fails
*
* @example
* ```typescript
* const result = await client.queryGraph(
* "MATCH (p:Person)-[:MENTIONED_IN]->(m:Meeting) RETURN p, m LIMIT 10"
* );
* console.log(result.nodes, result.edges);
* ```
*/
async queryGraph(cypher: string): Promise<GraphResult> {
const response = await this.fetchWithRetry(
`${this.baseUrl}/${this.apiVersion}/graph`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: cypher }),
},
);
if (!response.ok) {
throw new Error(`Graph query failed: ${response.statusText}`);
}
return (await response.json()) as GraphResult;
}
// ---- Temporal Delta Query ----
/**
* Query temporal deltas to see how content changed over time.
*
* Returns a sequence of diffs showing what was added and removed
* within the specified time range, optionally filtered by app or file.
*
* @param options - Delta query configuration
* @returns Array of delta results ordered chronologically
* @throws {Error} If the delta query fails
*
* @example
* ```typescript
* const deltas = await client.queryDelta({
* app: "VSCode",
* timeRange: {
* start: "2026-02-12T09:00:00Z",
* end: "2026-02-12T17:00:00Z",
* },
* includeChanges: true,
* });
* ```
*/
async queryDelta(options: DeltaQueryOptions): Promise<DeltaResult[]> {
const response = await this.fetchWithRetry(
`${this.baseUrl}/${this.apiVersion}/delta`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(options),
},
);
if (!response.ok) {
throw new Error(`Delta query failed: ${response.statusText}`);
}
return (await response.json()) as DeltaResult[];
}
// ---- Attention-Weighted Stream ----
/**
* Stream attention-weighted events from the OsPipe server.
*
* Yields events in real-time as they are detected by the attention model.
* Events are filtered by threshold and category. Uses Server-Sent Events (SSE).
*
* @param options - Stream configuration
* @returns Async generator of attention events
* @throws {Error} If the stream connection fails
*
* @example
* ```typescript
* for await (const event of client.streamAttention({
* threshold: 0.5,
* categories: ["code_change", "meeting_start"],
* })) {
* console.log(`[${event.category}] ${event.summary} (attention: ${event.attention})`);
* }
* ```
*/
async *streamAttention(
options: {
/** Minimum attention score to emit (0-1) */
threshold?: number;
/** Only emit events of these categories */
categories?: AttentionEvent["category"][];
/** AbortSignal to cancel the stream */
signal?: AbortSignal;
} = {}
): AsyncGenerator<AttentionEvent> {
const params = new URLSearchParams();
if (options.threshold !== undefined) {
params.set("threshold", options.threshold.toString());
}
if (options.categories) {
params.set("categories", options.categories.join(","));
}
if (options.signal?.aborted) {
throw new DOMException("The operation was aborted.", "AbortError");
}
const url = `${this.baseUrl}/${this.apiVersion}/stream/attention?${params}`;
const response = await this.fetchWithRetry(url, {
signal: options.signal,
});
if (!response.ok || !response.body) {
throw new Error(`Attention stream failed: ${response.statusText}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
if (options.signal?.aborted) {
break;
}
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6).trim();
if (data && data !== "[DONE]") {
yield JSON.parse(data) as AttentionEvent;
}
}
}
}
} finally {
reader.releaseLock();
}
}
// ---- Backward-Compatible Screenpipe API ----
/**
* Query the legacy Screenpipe search API (v1 compatible).
*
* This method provides backward compatibility with the original @screenpipe/js
* query interface. For enhanced features, use {@link queryRuVector} instead.
*
* @param options - Screenpipe-compatible query options
* @returns Array of search results
* @throws {Error} If the search request fails
*
* @example
* ```typescript
* const results = await client.queryScreenpipe({
* q: "meeting notes",
* contentType: "ocr",
* limit: 20,
* appName: "Notion",
* });
* ```
*/
async queryScreenpipe(options: {
/** Search query string */
q: string;
/** Content type filter */
contentType?: "all" | "ocr" | "audio";
/** Maximum number of results */
limit?: number;
/** Start of time range (ISO 8601) */
startTime?: string;
/** End of time range (ISO 8601) */
endTime?: string;
/** Filter by application name */
appName?: string;
}): Promise<SearchResult[]> {
const params = new URLSearchParams({ q: options.q });
if (options.contentType) params.set("content_type", options.contentType);
if (options.limit) params.set("limit", options.limit.toString());
if (options.startTime) params.set("start_time", options.startTime);
if (options.endTime) params.set("end_time", options.endTime);
if (options.appName) params.set("app_name", options.appName);
const response = await this.fetchWithRetry(`${this.baseUrl}/search?${params}`);
if (!response.ok) {
throw new Error(`Screenpipe search failed: ${response.statusText}`);
}
return (await response.json()) as SearchResult[];
}
// ---- Utilities ----
/**
* Determine the optimal query route for a given query string.
*
* The router analyzes the query intent and returns the best query mode
* (semantic, keyword, graph, temporal, or hybrid).
*
* @param query - Natural language query to route
* @returns The recommended query route
* @throws {Error} If the route request fails
*
* @example
* ```typescript
* const route = await client.routeQuery("who mentioned authentication yesterday?");
* // route === "graph" or "temporal"
* ```
*/
async routeQuery(query: string): Promise<QueryRoute> {
const response = await this.fetchWithRetry(
`${this.baseUrl}/${this.apiVersion}/route`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query }),
},
);
if (!response.ok) {
throw new Error(`Route failed: ${response.statusText}`);
}
const result = (await response.json()) as { route: QueryRoute };
return result.route;
}
/**
* Retrieve pipeline statistics from the OsPipe server.
*
* @returns Pipeline statistics including ingestion counts, storage, and uptime
* @throws {Error} If the stats request fails
*/
async stats(): Promise<PipelineStats> {
const response = await this.fetchWithRetry(
`${this.baseUrl}/${this.apiVersion}/stats`,
);
if (!response.ok) {
throw new Error(`Stats failed: ${response.statusText}`);
}
return (await response.json()) as PipelineStats;
}
/**
* Check the health of the OsPipe server.
*
* @returns Health status including version and active backends
* @throws {Error} If the health check fails
*/
async health(): Promise<{
status: string;
version: string;
backends: string[];
}> {
const response = await this.fetchWithRetry(
`${this.baseUrl}/${this.apiVersion}/health`,
);
if (!response.ok) {
throw new Error(`Health check failed: ${response.statusText}`);
}
return (await response.json()) as { status: string; version: string; backends: string[] };
}
}
// ---- Default Export ----
export default OsPipe;

View File

@@ -0,0 +1,118 @@
/**
* WASM bindings for OsPipe - use in browser-based pipes.
*
* This module provides a thin wrapper around the @ruvector/ospipe-wasm package,
* exposing vector search, embedding, deduplication, and safety checking
* capabilities that run entirely client-side via WebAssembly.
*
* @packageDocumentation
*/
/** A single search result from the WASM vector index. */
export interface WasmSearchResult {
/** Unique identifier for the indexed entry */
id: string;
/** Similarity score (higher is more similar) */
score: number;
/** JSON-encoded metadata string */
metadata: string;
}
/** Configuration options for WASM initialization. */
export interface OsPipeWasmOptions {
/** Embedding vector dimension (default: 384) */
dimension?: number;
}
/** The initialized WASM instance interface. */
export interface OsPipeWasmInstance {
/**
* Insert a vector into the index.
*
* @param id - Unique identifier for the entry
* @param embedding - Float32Array embedding vector
* @param metadata - JSON-encoded metadata string
* @param timestamp - Unix timestamp in milliseconds (default: Date.now())
*/
insert(id: string, embedding: Float32Array, metadata: string, timestamp?: number): void;
/**
* Search for the k nearest neighbors to the query embedding.
*
* @param queryEmbedding - Float32Array query vector
* @param k - Number of results to return (default: 10)
* @returns Array of search results ranked by similarity
*/
search(queryEmbedding: Float32Array, k?: number): WasmSearchResult[];
/**
* Search with a time range filter applied before ranking.
*
* @param queryEmbedding - Float32Array query vector
* @param k - Number of results to return
* @param startTime - Start of time range (Unix ms)
* @param endTime - End of time range (Unix ms)
* @returns Array of filtered search results
*/
searchFiltered(queryEmbedding: Float32Array, k: number, startTime: number, endTime: number): WasmSearchResult[];
/**
* Check if an embedding is a near-duplicate of an existing entry.
*
* @param embedding - Float32Array embedding to check
* @param threshold - Similarity threshold 0-1 (default: 0.95)
* @returns True if a duplicate is found above the threshold
*/
isDuplicate(embedding: Float32Array, threshold?: number): boolean;
/**
* Generate an embedding vector from text using the built-in ONNX model.
*
* @param text - Input text to embed
* @returns Float32Array embedding vector
*/
embedText(text: string): Float32Array;
/**
* Run a safety check on content, returning the recommended action.
*
* @param content - Content string to check
* @returns "allow", "redact", or "deny"
*/
safetyCheck(content: string): "allow" | "redact" | "deny";
/**
* Route a query to the optimal query type.
*
* @param query - Natural language query string
* @returns Recommended query route type
*/
routeQuery(query: string): string;
/** Number of entries currently in the index. */
readonly size: number;
/**
* Get index statistics as a JSON string.
*
* @returns JSON-encoded statistics object
*/
stats(): string;
}
/**
* Load and initialize the OsPipe WASM module.
*
* This function dynamically imports the @ruvector/ospipe-wasm package,
* initializes the WebAssembly module, and returns a typed wrapper
* around the raw WASM bindings.
*
* @param options - WASM initialization options
* @returns Initialized WASM instance with typed methods
* @throws {Error} If the WASM module fails to load or initialize
*
* @example
* ```typescript
* import { initOsPipeWasm } from "@ruvector/ospipe/wasm";
*
* const wasm = await initOsPipeWasm({ dimension: 384 });
*
* // Embed and insert
* const embedding = wasm.embedText("hello world");
* wasm.insert("doc-1", embedding, JSON.stringify({ app: "test" }));
*
* // Search
* const query = wasm.embedText("greetings");
* const results = wasm.search(query, 5);
* ```
*/
export declare function initOsPipeWasm(options?: OsPipeWasmOptions): Promise<OsPipeWasmInstance>;
//# sourceMappingURL=wasm.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"wasm.d.ts","sourceRoot":"","sources":["wasm.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,yDAAyD;AACzD,MAAM,WAAW,gBAAgB;IAC/B,8CAA8C;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qDAAqD;AACrD,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAExF;;;;;;OAMG;IACH,MAAM,CAAC,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAAC;IAErE;;;;;;;;OAQG;IACH,cAAc,CACZ,cAAc,EAAE,YAAY,EAC5B,CAAC,EAAE,MAAM,EACT,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,gBAAgB,EAAE,CAAC;IAEtB;;;;;;OAMG;IACH,WAAW,CAAC,SAAS,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAElE;;;;;OAKG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAAC;IAEtC;;;;;OAKG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;IAE1D;;;;;OAKG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAElC,gDAAgD;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;;OAIG;IACH,KAAK,IAAI,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,cAAc,CAClC,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,kBAAkB,CAAC,CAqE7B"}

View File

@@ -0,0 +1,120 @@
"use strict";
/**
* WASM bindings for OsPipe - use in browser-based pipes.
*
* This module provides a thin wrapper around the @ruvector/ospipe-wasm package,
* exposing vector search, embedding, deduplication, and safety checking
* capabilities that run entirely client-side via WebAssembly.
*
* @packageDocumentation
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.initOsPipeWasm = initOsPipeWasm;
/**
* Load and initialize the OsPipe WASM module.
*
* This function dynamically imports the @ruvector/ospipe-wasm package,
* initializes the WebAssembly module, and returns a typed wrapper
* around the raw WASM bindings.
*
* @param options - WASM initialization options
* @returns Initialized WASM instance with typed methods
* @throws {Error} If the WASM module fails to load or initialize
*
* @example
* ```typescript
* import { initOsPipeWasm } from "@ruvector/ospipe/wasm";
*
* const wasm = await initOsPipeWasm({ dimension: 384 });
*
* // Embed and insert
* const embedding = wasm.embedText("hello world");
* wasm.insert("doc-1", embedding, JSON.stringify({ app: "test" }));
*
* // Search
* const query = wasm.embedText("greetings");
* const results = wasm.search(query, 5);
* ```
*/
async function initOsPipeWasm(options = {}) {
const dimension = options.dimension ?? 384;
// Dynamic import so the WASM package is not required at bundle time.
// This allows the main @ruvector/ospipe package to work without WASM.
// The @ruvector/ospipe-wasm package provides the compiled WASM bindings.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let wasm;
try {
// Use a variable to prevent TypeScript from resolving the module statically
const wasmPkg = "@ruvector/ospipe-wasm";
wasm = await Promise.resolve(`${wasmPkg}`).then(s => __importStar(require(s)));
}
catch {
throw new Error("Failed to load @ruvector/ospipe-wasm. " +
"Install it with: npm install @ruvector/ospipe-wasm");
}
await wasm.default();
const instance = new wasm.OsPipeWasm(dimension);
return {
insert(id, embedding, metadata, timestamp) {
instance.insert(id, embedding, metadata, timestamp ?? Date.now());
},
search(queryEmbedding, k = 10) {
return instance.search(queryEmbedding, k);
},
searchFiltered(queryEmbedding, k, startTime, endTime) {
return instance.search_filtered(queryEmbedding, k, startTime, endTime);
},
isDuplicate(embedding, threshold = 0.95) {
return instance.is_duplicate(embedding, threshold);
},
embedText(text) {
return new Float32Array(instance.embed_text(text));
},
safetyCheck(content) {
return instance.safety_check(content);
},
routeQuery(query) {
return instance.route_query(query);
},
get size() {
return instance.len();
},
stats() {
return instance.stats();
},
};
}
//# sourceMappingURL=wasm.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"wasm.js","sourceRoot":"","sources":["wasm.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6HH,wCAuEC;AAjGD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACI,KAAK,UAAU,cAAc,CAClC,UAA6B,EAAE;IAE/B,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,GAAG,CAAC;IAE3C,qEAAqE;IACrE,sEAAsE;IACtE,yEAAyE;IACzE,8DAA8D;IAC9D,IAAI,IAAS,CAAC;IACd,IAAI,CAAC;QACH,4EAA4E;QAC5E,MAAM,OAAO,GAAG,uBAAuB,CAAC;QACxC,IAAI,GAAG,yBAAuC,OAAO,uCAAC,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,wCAAwC;YACtC,oDAAoD,CACvD,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IAErB,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IAEhD,OAAO;QACL,MAAM,CACJ,EAAU,EACV,SAAuB,EACvB,QAAgB,EAChB,SAAkB;YAElB,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,CAAC;QAED,MAAM,CAAC,cAA4B,EAAE,CAAC,GAAG,EAAE;YACzC,OAAO,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC;QAC5C,CAAC;QAED,cAAc,CACZ,cAA4B,EAC5B,CAAS,EACT,SAAiB,EACjB,OAAe;YAEf,OAAO,QAAQ,CAAC,eAAe,CAAC,cAAc,EAAE,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;QACzE,CAAC;QAED,WAAW,CAAC,SAAuB,EAAE,SAAS,GAAG,IAAI;YACnD,OAAO,QAAQ,CAAC,YAAY,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACrD,CAAC;QAED,SAAS,CAAC,IAAY;YACpB,OAAO,IAAI,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QACrD,CAAC;QAED,WAAW,CAAC,OAAe;YACzB,OAAO,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAgC,CAAC;QACvE,CAAC;QAED,UAAU,CAAC,KAAa;YACtB,OAAO,QAAQ,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QACrC,CAAC;QAED,IAAI,IAAI;YACN,OAAO,QAAQ,CAAC,GAAG,EAAE,CAAC;QACxB,CAAC;QAED,KAAK;YACH,OAAO,QAAQ,CAAC,KAAK,EAAE,CAAC;QAC1B,CAAC;KACF,CAAC;AACJ,CAAC"}

View File

@@ -0,0 +1,205 @@
/**
* WASM bindings for OsPipe - use in browser-based pipes.
*
* This module provides a thin wrapper around the @ruvector/ospipe-wasm package,
* exposing vector search, embedding, deduplication, and safety checking
* capabilities that run entirely client-side via WebAssembly.
*
* @packageDocumentation
*/
/** A single search result from the WASM vector index. */
export interface WasmSearchResult {
/** Unique identifier for the indexed entry */
id: string;
/** Similarity score (higher is more similar) */
score: number;
/** JSON-encoded metadata string */
metadata: string;
}
/** Configuration options for WASM initialization. */
export interface OsPipeWasmOptions {
/** Embedding vector dimension (default: 384) */
dimension?: number;
}
/** The initialized WASM instance interface. */
export interface OsPipeWasmInstance {
/**
* Insert a vector into the index.
*
* @param id - Unique identifier for the entry
* @param embedding - Float32Array embedding vector
* @param metadata - JSON-encoded metadata string
* @param timestamp - Unix timestamp in milliseconds (default: Date.now())
*/
insert(id: string, embedding: Float32Array, metadata: string, timestamp?: number): void;
/**
* Search for the k nearest neighbors to the query embedding.
*
* @param queryEmbedding - Float32Array query vector
* @param k - Number of results to return (default: 10)
* @returns Array of search results ranked by similarity
*/
search(queryEmbedding: Float32Array, k?: number): WasmSearchResult[];
/**
* Search with a time range filter applied before ranking.
*
* @param queryEmbedding - Float32Array query vector
* @param k - Number of results to return
* @param startTime - Start of time range (Unix ms)
* @param endTime - End of time range (Unix ms)
* @returns Array of filtered search results
*/
searchFiltered(
queryEmbedding: Float32Array,
k: number,
startTime: number,
endTime: number
): WasmSearchResult[];
/**
* Check if an embedding is a near-duplicate of an existing entry.
*
* @param embedding - Float32Array embedding to check
* @param threshold - Similarity threshold 0-1 (default: 0.95)
* @returns True if a duplicate is found above the threshold
*/
isDuplicate(embedding: Float32Array, threshold?: number): boolean;
/**
* Generate an embedding vector from text using the built-in ONNX model.
*
* @param text - Input text to embed
* @returns Float32Array embedding vector
*/
embedText(text: string): Float32Array;
/**
* Run a safety check on content, returning the recommended action.
*
* @param content - Content string to check
* @returns "allow", "redact", or "deny"
*/
safetyCheck(content: string): "allow" | "redact" | "deny";
/**
* Route a query to the optimal query type.
*
* @param query - Natural language query string
* @returns Recommended query route type
*/
routeQuery(query: string): string;
/** Number of entries currently in the index. */
readonly size: number;
/**
* Get index statistics as a JSON string.
*
* @returns JSON-encoded statistics object
*/
stats(): string;
}
/**
* Load and initialize the OsPipe WASM module.
*
* This function dynamically imports the @ruvector/ospipe-wasm package,
* initializes the WebAssembly module, and returns a typed wrapper
* around the raw WASM bindings.
*
* @param options - WASM initialization options
* @returns Initialized WASM instance with typed methods
* @throws {Error} If the WASM module fails to load or initialize
*
* @example
* ```typescript
* import { initOsPipeWasm } from "@ruvector/ospipe/wasm";
*
* const wasm = await initOsPipeWasm({ dimension: 384 });
*
* // Embed and insert
* const embedding = wasm.embedText("hello world");
* wasm.insert("doc-1", embedding, JSON.stringify({ app: "test" }));
*
* // Search
* const query = wasm.embedText("greetings");
* const results = wasm.search(query, 5);
* ```
*/
export async function initOsPipeWasm(
options: OsPipeWasmOptions = {}
): Promise<OsPipeWasmInstance> {
const dimension = options.dimension ?? 384;
// Dynamic import so the WASM package is not required at bundle time.
// This allows the main @ruvector/ospipe package to work without WASM.
// The @ruvector/ospipe-wasm package provides the compiled WASM bindings.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let wasm: any;
try {
// Use a variable to prevent TypeScript from resolving the module statically
const wasmPkg = "@ruvector/ospipe-wasm";
wasm = await import(/* webpackIgnore: true */ wasmPkg);
} catch {
throw new Error(
"Failed to load @ruvector/ospipe-wasm. " +
"Install it with: npm install @ruvector/ospipe-wasm"
);
}
await wasm.default();
const instance = new wasm.OsPipeWasm(dimension);
return {
insert(
id: string,
embedding: Float32Array,
metadata: string,
timestamp?: number
): void {
instance.insert(id, embedding, metadata, timestamp ?? Date.now());
},
search(queryEmbedding: Float32Array, k = 10): WasmSearchResult[] {
return instance.search(queryEmbedding, k);
},
searchFiltered(
queryEmbedding: Float32Array,
k: number,
startTime: number,
endTime: number
): WasmSearchResult[] {
return instance.search_filtered(queryEmbedding, k, startTime, endTime);
},
isDuplicate(embedding: Float32Array, threshold = 0.95): boolean {
return instance.is_duplicate(embedding, threshold);
},
embedText(text: string): Float32Array {
return new Float32Array(instance.embed_text(text));
},
safetyCheck(content: string): "allow" | "redact" | "deny" {
return instance.safety_check(content) as "allow" | "redact" | "deny";
},
routeQuery(query: string): string {
return instance.route_query(query);
},
get size(): number {
return instance.len();
},
stats(): string {
return instance.stats();
},
};
}