ruflo 3.6.12 → 3.6.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +4 -1
  2. package/src/ruvocal/.claude-flow/data/pending-insights.jsonl +25 -0
  3. package/src/ruvocal/.claude-flow/neural/stats.json +6 -0
  4. package/src/ruvocal/.dockerignore +5 -1
  5. package/src/ruvocal/.gcloudignore +18 -0
  6. package/src/ruvocal/README.md +107 -133
  7. package/src/ruvocal/cloudbuild.yaml +68 -0
  8. package/src/ruvocal/config/branding.env.example +19 -0
  9. package/src/ruvocal/mcp-bridge/index.js +15 -1
  10. package/src/ruvocal/src/lib/components/FoundationBackground.svelte +242 -0
  11. package/src/ruvocal/src/lib/components/NavMenu.svelte +18 -0
  12. package/src/ruvocal/src/lib/components/RufloHelpModal.svelte +411 -0
  13. package/src/ruvocal/src/lib/components/chat/ChatWindow.svelte +122 -4
  14. package/src/ruvocal/src/lib/components/wasm/GalleryPanel.svelte +357 -0
  15. package/src/ruvocal/src/lib/constants/mcpExamples.ts +56 -77
  16. package/src/ruvocal/src/lib/constants/routerExamples.ts +51 -127
  17. package/src/ruvocal/src/lib/constants/rvagentPresets.ts +206 -0
  18. package/src/ruvocal/src/lib/server/textGeneration/mcp/wasmTools.test.ts +633 -0
  19. package/src/ruvocal/src/lib/stores/mcpServers.ts +195 -6
  20. package/src/ruvocal/src/lib/stores/wasmMcp.ts +472 -0
  21. package/src/ruvocal/src/lib/types/Settings.ts +7 -0
  22. package/src/ruvocal/src/lib/types/Tool.ts +4 -1
  23. package/src/ruvocal/src/lib/wasm/idb.ts +438 -0
  24. package/src/ruvocal/src/lib/wasm/index.ts +1213 -0
  25. package/src/ruvocal/src/lib/wasm/tests/wasm-capabilities.test.ts +565 -0
  26. package/src/ruvocal/src/lib/wasm/wasm.worker.ts +332 -0
  27. package/src/ruvocal/src/lib/wasm/workerClient.ts +166 -0
  28. package/src/ruvocal/static/wasm/rvagent_wasm.js +1539 -0
  29. package/src/ruvocal/static/wasm/rvagent_wasm_bg.wasm +0 -0
@@ -0,0 +1,332 @@
1
+ /**
2
+ * WASM MCP Web Worker — Off-main-thread JSON-RPC server.
3
+ *
4
+ * Owns one WasmMcpServer + one WasmGallery instance and proxies the MCP
5
+ * surface (handle_message) plus the most common gallery operations
6
+ * (list / search / count / setActive / getActive) so the main thread
7
+ * never blocks on the ~300ms WASM compile or on tool execution.
8
+ *
9
+ * Wire format (request → main → worker):
10
+ * { id: number, method: "load" | "callMcp" | "gallery.X", params?: unknown }
11
+ *
12
+ * Wire format (response → worker → main):
13
+ * { id: number, result?: unknown, error?: string }
14
+ */
15
+
16
+ // NOTE: This file runs in a Web Worker context. It deliberately does NOT
17
+ // import from $lib/wasm (which pulls in $app/environment — a SvelteKit
18
+ // runtime module not available outside the page bundle). Types are
19
+ // duplicated locally; the worker keeps a minimal mock fallback identical
20
+ // in shape to the one in $lib/wasm/index.ts.
21
+
22
+ interface WasmMcpServer {
23
+ handle_message(message: string): string;
24
+ }
25
+
26
+ interface WasmGallery {
27
+ list(): unknown[];
28
+ listByCategory(category: string): unknown[];
29
+ search(query: string): unknown[];
30
+ get(id: string): unknown;
31
+ setActive(id: string): void;
32
+ getActive(): string | null;
33
+ count(): number;
34
+ getCategories(): Record<string, number>;
35
+ }
36
+
37
+ interface WorkerRequest {
38
+ id: number;
39
+ method: string;
40
+ params?: unknown;
41
+ }
42
+
43
+ interface WorkerResponse {
44
+ id: number;
45
+ result?: unknown;
46
+ error?: string;
47
+ }
48
+
49
+ let mcpServer: WasmMcpServer | null = null;
50
+ let gallery: WasmGallery | null = null;
51
+ let loadPromise: Promise<void> | null = null;
52
+
53
+ const ctx = self as unknown as Worker;
54
+
55
+ function reply(id: number, result?: unknown, error?: string) {
56
+ const msg: WorkerResponse = { id };
57
+ if (error !== undefined) msg.error = error;
58
+ else msg.result = result;
59
+ ctx.postMessage(msg);
60
+ }
61
+
62
+ /**
63
+ * Worker-safe mock WasmMcpServer. The real WASM bundle is loaded by the
64
+ * page bundle today; once we wire `static/wasm/rvagent_wasm.js` directly
65
+ * into the worker via `import("/wasm/rvagent_wasm.js")`, this mock acts
66
+ * as the fallback when the network fetch fails.
67
+ */
68
+ function createMockServer(): WasmMcpServer {
69
+ const fs = new Map<string, string>();
70
+
71
+ return {
72
+ handle_message(raw: string): string {
73
+ let req: { id?: number | string; method?: string; params?: Record<string, unknown> };
74
+ try {
75
+ req = JSON.parse(raw);
76
+ } catch {
77
+ return JSON.stringify({
78
+ jsonrpc: "2.0",
79
+ id: null,
80
+ error: { code: -32700, message: "Parse error" },
81
+ });
82
+ }
83
+
84
+ const { id = null, method, params } = req;
85
+
86
+ if (method === "initialize") {
87
+ return JSON.stringify({
88
+ jsonrpc: "2.0",
89
+ id,
90
+ result: {
91
+ protocolVersion: "2024-11-05",
92
+ capabilities: { tools: {}, prompts: {} },
93
+ serverInfo: { name: "rvagent-wasm-worker-mock", version: "1.0.0" },
94
+ },
95
+ });
96
+ }
97
+
98
+ if (method === "tools/list") {
99
+ return JSON.stringify({
100
+ jsonrpc: "2.0",
101
+ id,
102
+ result: {
103
+ tools: [
104
+ {
105
+ name: "read_file",
106
+ description: "Read a file from the virtual filesystem (worker mock)",
107
+ inputSchema: {
108
+ type: "object",
109
+ properties: { path: { type: "string" } },
110
+ required: ["path"],
111
+ },
112
+ },
113
+ {
114
+ name: "write_file",
115
+ description: "Write a file to the virtual filesystem (worker mock)",
116
+ inputSchema: {
117
+ type: "object",
118
+ properties: {
119
+ path: { type: "string" },
120
+ content: { type: "string" },
121
+ },
122
+ required: ["path", "content"],
123
+ },
124
+ },
125
+ {
126
+ name: "list_files",
127
+ description: "List virtual filesystem entries (worker mock)",
128
+ inputSchema: { type: "object", properties: {} },
129
+ },
130
+ ],
131
+ },
132
+ });
133
+ }
134
+
135
+ if (method === "tools/call") {
136
+ const p = params as { name?: string; arguments?: Record<string, unknown> } | undefined;
137
+ const name = p?.name;
138
+ const args = (p?.arguments ?? {}) as Record<string, unknown>;
139
+
140
+ if (name === "write_file") {
141
+ const path = String(args.path ?? "");
142
+ const content = String(args.content ?? "");
143
+ fs.set(path, content);
144
+ return JSON.stringify({
145
+ jsonrpc: "2.0",
146
+ id,
147
+ result: { content: [{ type: "text", text: `wrote ${content.length} bytes to ${path}` }] },
148
+ });
149
+ }
150
+ if (name === "read_file") {
151
+ const path = String(args.path ?? "");
152
+ const content = fs.get(path);
153
+ if (content === undefined) {
154
+ return JSON.stringify({
155
+ jsonrpc: "2.0",
156
+ id,
157
+ error: { code: -32602, message: `file not found: ${path}` },
158
+ });
159
+ }
160
+ return JSON.stringify({
161
+ jsonrpc: "2.0",
162
+ id,
163
+ result: { content: [{ type: "text", text: content }] },
164
+ });
165
+ }
166
+ if (name === "list_files") {
167
+ return JSON.stringify({
168
+ jsonrpc: "2.0",
169
+ id,
170
+ result: {
171
+ content: [{ type: "text", text: JSON.stringify([...fs.keys()]) }],
172
+ },
173
+ });
174
+ }
175
+
176
+ return JSON.stringify({
177
+ jsonrpc: "2.0",
178
+ id,
179
+ error: { code: -32601, message: `Method not found: ${name}` },
180
+ });
181
+ }
182
+
183
+ if (method === "prompts/list") {
184
+ return JSON.stringify({ jsonrpc: "2.0", id, result: { prompts: [] } });
185
+ }
186
+
187
+ return JSON.stringify({
188
+ jsonrpc: "2.0",
189
+ id,
190
+ error: { code: -32601, message: `Method not found: ${method}` },
191
+ });
192
+ },
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Worker-safe mock gallery. Replace with the real `WasmGallery` constructor
198
+ * once `import("/wasm/rvagent_wasm.js")` works in worker context.
199
+ */
200
+ function createMockGallery(): WasmGallery {
201
+ const builtins: Array<{ id: string; name: string; description: string; category: string }> = [
202
+ {
203
+ id: "blank",
204
+ name: "Blank Template",
205
+ description: "Empty starting point for custom MCP work.",
206
+ category: "starter",
207
+ },
208
+ {
209
+ id: "research",
210
+ name: "Research Assistant",
211
+ description: "Web search + note-taking with persistent memory.",
212
+ category: "knowledge",
213
+ },
214
+ ];
215
+
216
+ let activeId: string | null = null;
217
+
218
+ return {
219
+ list: () => builtins.slice(),
220
+ listByCategory: (cat) => builtins.filter((t) => t.category === cat),
221
+ search: (q) => {
222
+ const needle = q.toLowerCase();
223
+ return builtins
224
+ .filter((t) => t.name.toLowerCase().includes(needle) || t.description.toLowerCase().includes(needle))
225
+ .map((t, idx) => ({ ...t, relevance: 1 - idx * 0.1, tags: [] }));
226
+ },
227
+ get: (id) => {
228
+ const t = builtins.find((b) => b.id === id);
229
+ if (!t) throw new Error(`Template not found: ${id}`);
230
+ return t;
231
+ },
232
+ setActive: (id) => {
233
+ activeId = id;
234
+ },
235
+ getActive: () => activeId,
236
+ count: () => builtins.length,
237
+ getCategories: () => {
238
+ const out: Record<string, number> = {};
239
+ for (const t of builtins) out[t.category] = (out[t.category] ?? 0) + 1;
240
+ return out;
241
+ },
242
+ };
243
+ }
244
+
245
+ async function ensureLoaded(): Promise<void> {
246
+ if (mcpServer && gallery) return;
247
+ if (loadPromise) return loadPromise;
248
+
249
+ loadPromise = (async () => {
250
+ // TODO: load real rvagent_wasm.js via dynamic import once the static
251
+ // bundle exposes a worker-friendly init. Mock is functionally complete
252
+ // for the chat-ui's MCP integration test surface.
253
+ mcpServer = createMockServer();
254
+ gallery = createMockGallery();
255
+
256
+ const initReq = JSON.stringify({
257
+ jsonrpc: "2.0",
258
+ id: 1,
259
+ method: "initialize",
260
+ params: {
261
+ protocolVersion: "2024-11-05",
262
+ clientInfo: { name: "ruvocal-ui-worker", version: "1.0.0" },
263
+ },
264
+ });
265
+ const initRes = JSON.parse(mcpServer.handle_message(initReq));
266
+ if (initRes.error) throw new Error(initRes.error.message ?? "MCP init failed");
267
+ })();
268
+
269
+ await loadPromise;
270
+ }
271
+
272
+ ctx.addEventListener("message", async (event: MessageEvent<WorkerRequest>) => {
273
+ const { id, method, params } = event.data;
274
+
275
+ try {
276
+ await ensureLoaded();
277
+ if (!mcpServer || !gallery) throw new Error("WASM not initialized");
278
+
279
+ switch (method) {
280
+ case "load":
281
+ reply(id, true);
282
+ return;
283
+
284
+ case "callMcp": {
285
+ const message = JSON.stringify(params);
286
+ const response = mcpServer.handle_message(message);
287
+ reply(id, JSON.parse(response));
288
+ return;
289
+ }
290
+
291
+ case "gallery.list":
292
+ reply(id, gallery.list());
293
+ return;
294
+
295
+ case "gallery.listByCategory":
296
+ reply(id, gallery.listByCategory(params as string));
297
+ return;
298
+
299
+ case "gallery.search":
300
+ reply(id, gallery.search(params as string));
301
+ return;
302
+
303
+ case "gallery.get":
304
+ reply(id, gallery.get(params as string));
305
+ return;
306
+
307
+ case "gallery.setActive":
308
+ gallery.setActive(params as string);
309
+ reply(id, true);
310
+ return;
311
+
312
+ case "gallery.getActive":
313
+ reply(id, gallery.getActive());
314
+ return;
315
+
316
+ case "gallery.count":
317
+ reply(id, gallery.count());
318
+ return;
319
+
320
+ case "gallery.getCategories":
321
+ reply(id, gallery.getCategories());
322
+ return;
323
+
324
+ default:
325
+ reply(id, undefined, `Unknown method: ${method}`);
326
+ }
327
+ } catch (err) {
328
+ reply(id, undefined, err instanceof Error ? err.message : String(err));
329
+ }
330
+ });
331
+
332
+ ctx.postMessage({ id: 0, type: "ready" });
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Main-thread client for the WASM MCP Web Worker.
3
+ *
4
+ * Lazily spawns the worker, multiplexes requests with auto-incrementing IDs,
5
+ * and exposes a typed Promise-returning surface that mirrors the subset of
6
+ * WasmMcpServer / WasmGallery the chat UI needs.
7
+ *
8
+ * Behind a feature flag so the existing main-thread code path remains the
9
+ * default until we've broken everything in `wasmMcp.ts` over to the worker.
10
+ * Toggle with `localStorage.setItem("ruflo:wasm-worker", "true")` from the
11
+ * browser console, or pass `?worker=1` on the URL.
12
+ */
13
+
14
+ import { browser } from "$app/environment";
15
+ import type { GalleryTemplate, SearchResult } from "./index";
16
+
17
+ interface PendingRequest {
18
+ resolve: (value: unknown) => void;
19
+ reject: (reason: Error) => void;
20
+ }
21
+
22
+ interface JsonRpcRequest {
23
+ jsonrpc: "2.0";
24
+ id: number | string;
25
+ method: string;
26
+ params?: unknown;
27
+ }
28
+
29
+ interface JsonRpcResponse {
30
+ jsonrpc: "2.0";
31
+ id: number | string | null;
32
+ result?: unknown;
33
+ error?: { code: number; message: string; data?: unknown };
34
+ }
35
+
36
+ let worker: Worker | null = null;
37
+ let nextId = 1;
38
+ const pending = new Map<number, PendingRequest>();
39
+ let readyPromise: Promise<void> | null = null;
40
+
41
+ export function isWorkerEnabled(): boolean {
42
+ if (!browser) return false;
43
+ try {
44
+ const flag = localStorage.getItem("ruflo:wasm-worker");
45
+ if (flag === "true") return true;
46
+ const params = new URLSearchParams(window.location.search);
47
+ return params.get("worker") === "1";
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ function ensureWorker(): Worker {
54
+ if (worker) return worker;
55
+ // Vite resolves `?worker` import to a Worker constructor.
56
+ // Using new URL() so vite-plugin-svelte picks it up at build time.
57
+ worker = new Worker(new URL("./wasm.worker.ts", import.meta.url), {
58
+ type: "module",
59
+ name: "ruflo-wasm-mcp",
60
+ });
61
+
62
+ worker.addEventListener("message", (event: MessageEvent) => {
63
+ const data = event.data as { id: number; result?: unknown; error?: string; type?: string };
64
+ if (data?.type === "ready") return; // readiness ping
65
+ if (typeof data?.id !== "number") return;
66
+ const slot = pending.get(data.id);
67
+ if (!slot) return;
68
+ pending.delete(data.id);
69
+ if (data.error) slot.reject(new Error(data.error));
70
+ else slot.resolve(data.result);
71
+ });
72
+
73
+ worker.addEventListener("error", (e: ErrorEvent) => {
74
+ console.error("[wasm.worker] error:", e.message);
75
+ // Reject all in-flight requests so callers don't hang.
76
+ for (const [, slot] of pending) slot.reject(new Error(`worker error: ${e.message}`));
77
+ pending.clear();
78
+ });
79
+
80
+ return worker;
81
+ }
82
+
83
+ function call<T = unknown>(method: string, params?: unknown): Promise<T> {
84
+ const w = ensureWorker();
85
+ const id = nextId++;
86
+ return new Promise<T>((resolve, reject) => {
87
+ pending.set(id, { resolve: resolve as (v: unknown) => void, reject });
88
+ w.postMessage({ id, method, params });
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Ensure the worker has finished its WASM load. Idempotent — first caller
94
+ * pays the ~300ms compile cost off the main thread; subsequent calls are
95
+ * no-ops that resolve when the same in-flight load finishes.
96
+ */
97
+ export function ensureReady(): Promise<void> {
98
+ if (!readyPromise) readyPromise = call<boolean>("load").then(() => undefined);
99
+ return readyPromise;
100
+ }
101
+
102
+ /**
103
+ * Send a JSON-RPC message to the WASM MCP server in the worker.
104
+ */
105
+ export async function callMcpInWorker(
106
+ method: string,
107
+ params?: unknown
108
+ ): Promise<JsonRpcResponse> {
109
+ await ensureReady();
110
+ const request: JsonRpcRequest = {
111
+ jsonrpc: "2.0",
112
+ id: nextId++,
113
+ method,
114
+ params,
115
+ };
116
+ return call<JsonRpcResponse>("callMcp", request);
117
+ }
118
+
119
+ export async function listTemplatesInWorker(): Promise<GalleryTemplate[]> {
120
+ await ensureReady();
121
+ return call<GalleryTemplate[]>("gallery.list");
122
+ }
123
+
124
+ export async function searchTemplatesInWorker(query: string): Promise<SearchResult[]> {
125
+ await ensureReady();
126
+ return call<SearchResult[]>("gallery.search", query);
127
+ }
128
+
129
+ export async function getTemplateInWorker(id: string): Promise<GalleryTemplate> {
130
+ await ensureReady();
131
+ return call<GalleryTemplate>("gallery.get", id);
132
+ }
133
+
134
+ export async function setActiveTemplateInWorker(id: string): Promise<void> {
135
+ await ensureReady();
136
+ await call("gallery.setActive", id);
137
+ }
138
+
139
+ export async function getActiveTemplateInWorker(): Promise<string | null> {
140
+ await ensureReady();
141
+ return call<string | null>("gallery.getActive");
142
+ }
143
+
144
+ export async function getCategoriesInWorker(): Promise<Record<string, number>> {
145
+ await ensureReady();
146
+ return call<Record<string, number>>("gallery.getCategories");
147
+ }
148
+
149
+ export async function templateCountInWorker(): Promise<number> {
150
+ await ensureReady();
151
+ return call<number>("gallery.count");
152
+ }
153
+
154
+ /**
155
+ * Tear down the worker (for tests or explicit cleanup).
156
+ */
157
+ export function disposeWorker(): void {
158
+ if (worker) {
159
+ worker.terminate();
160
+ worker = null;
161
+ }
162
+ for (const [, slot] of pending) slot.reject(new Error("worker disposed"));
163
+ pending.clear();
164
+ readyPromise = null;
165
+ nextId = 1;
166
+ }