@decido/kernel-bridge 1.0.0 → 4.0.1

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.
@@ -1,326 +0,0 @@
1
- /**
2
- * OllamaService — Chat interface for DecidoOS Agent
3
- *
4
- * Manages conversation history, system prompt construction,
5
- * and tool-call parsing. Chat requests are now routed through
6
- * InferenceRouter to support multiple LLM providers.
7
- */
8
-
9
- import { routeChat } from './InferenceRouter';
10
- import type { ChatMessage } from './providers/LLMProvider';
11
-
12
- // ─── Types ──────────────────────────────────────────────────
13
-
14
- interface OllamaChatMessage {
15
- role: 'system' | 'user' | 'assistant';
16
- content: string;
17
- }
18
-
19
- interface OllamaModelInfo {
20
- name: string;
21
- size: number;
22
- modified_at: string;
23
- }
24
-
25
- // ─── Tool Call Types ────────────────────────────────────────
26
-
27
- export interface ToolCallRequest {
28
- name: string;
29
- args: Record<string, unknown>;
30
- }
31
-
32
- export interface ChatWithToolsResult {
33
- text: string;
34
- toolCalls: ToolCallRequest[];
35
- }
36
-
37
- // ─── Config ─────────────────────────────────────────────────
38
-
39
- const OLLAMA_BASE_URL = 'http://localhost:11434';
40
- const DEFAULT_MODEL = 'qwen2:latest';
41
-
42
- // ─── System Prompt ──────────────────────────────────────────
43
-
44
- function buildSystemPrompt(): string {
45
- const liveContext = buildLiveContext();
46
- const toolSchemas = ''; // Removed toolRegistry reference to fix dependencies, but kept variable for now
47
-
48
- return `- Estás integrado en DecidoOS, una plataforma empresarial de escritorio
49
- - Corres localmente en la máquina del usuario — todo es privado
50
- - Puedes ejecutar herramientas del sistema para ayudar al usuario
51
-
52
- ${liveContext}
53
-
54
- ## Herramientas disponibles
55
-
56
- ${toolSchemas}
57
-
58
- ## Reglas de respuesta
59
- 1. Responde siempre en español a menos que el usuario hable en otro idioma
60
- 2. Sé directo — no des introducciones largas
61
- 3. Si puedes resolver algo con una herramienta, USA LA HERRAMIENTA nativa en vez de solo describirla
62
- 4. Mantén respuestas bajo 300 palabras
63
- 5. Si no sabes algo, dilo honestamente
64
- 6. NO inventes resultados de herramientas — espera el resultado real
65
- 7. Cuando el usuario pregunte sobre el estado del sistema, USA LOS DATOS del "Estado actual del sistema" que tienes arriba — esos son datos reales y recientes`;
66
- }
67
-
68
- // ─── Live Context Builder ───────────────────────────────────
69
-
70
- /**
71
- * Builds a live system context section from watchdog metrics
72
- * and the app's context snapshot. Called every time a message
73
- * is sent to keep the LLM's awareness current.
74
- */
75
- function buildLiveContext(): string {
76
- const parts: string[] = ['## Estado actual del sistema'];
77
- const now = new Date().toLocaleString('es-CO', { hour12: false });
78
- parts.push(`Hora actual: ${now}`);
79
-
80
- // Watchdog metrics
81
- try {
82
- // Dynamic import to avoid circular deps — we access synchronously via singleton
83
- const watchdogModule = (globalThis as any).__systemWatchdog;
84
- if (watchdogModule) {
85
- const snapshot = watchdogModule.getLastSnapshot?.();
86
- if (snapshot) {
87
- const metrics: string[] = [];
88
- if (snapshot.cpuPercent !== null) metrics.push(`CPU: ${snapshot.cpuPercent.toFixed(1)}%`);
89
- if (snapshot.memoryPercent !== null) metrics.push(`Memoria: ${snapshot.memoryPercent.toFixed(1)}%`);
90
- if (snapshot.diskFreeGB !== null) metrics.push(`Disco libre: ${snapshot.diskFreeGB.toFixed(1)} GB`);
91
- if (snapshot.connectionCount !== null) metrics.push(`Conexiones de red: ${snapshot.connectionCount}`);
92
- if (metrics.length > 0) {
93
- parts.push(`Métricas del sistema: ${metrics.join(' | ')}`);
94
- }
95
- }
96
-
97
- const alerts = watchdogModule.getAlerts?.()?.filter((a: any) => !a.dismissed).slice(-5) ?? [];
98
- if (alerts.length > 0) {
99
- parts.push('Alertas recientes:');
100
- for (const alert of alerts) {
101
- const emoji = alert.severity === 'critical' ? '🚨' : '⚠️';
102
- parts.push(` ${emoji} ${alert.title}`);
103
- }
104
- }
105
- }
106
- } catch {
107
- // Watchdog not available yet
108
- }
109
-
110
- // Context snapshot from store (accessed via globalThis to avoid monolith coupling)
111
- try {
112
- const appStore = (globalThis as any).__appStore;
113
- const ctx = appStore?.getState?.()?.contextSnapshot;
114
- if (ctx) {
115
- if (ctx.canvasNodeCount > 0) parts.push(`Canvas: ${ctx.canvasNodeCount} nodos`);
116
- if (ctx.gitBranch) parts.push(`Git: rama ${ctx.gitBranch}${ctx.gitModifiedFiles ? ` (${ctx.gitModifiedFiles} archivos modificados)` : ''}`);
117
- if (ctx.activeInsights > 0) parts.push(`Insights activos: ${ctx.activeInsights}`);
118
- if (ctx.criticalInsightsSummary?.length > 0) {
119
- parts.push('Insights críticos: ' + ctx.criticalInsightsSummary.slice(0, 3).join('; '));
120
- }
121
- }
122
- } catch {
123
- // Store not available
124
- }
125
-
126
- // Persistent memory (learned facts)
127
- try {
128
- const memoryModule = (globalThis as any).__agentMemory;
129
- if (memoryModule) {
130
- const memContext = memoryModule.buildMemoryContext?.();
131
- if (memContext) {
132
- parts.push('');
133
- parts.push(memContext);
134
- }
135
- }
136
- } catch {
137
- // Memory not available
138
- }
139
-
140
- return parts.join('\n');
141
- }
142
-
143
- // ─── Tool Call Parser ───────────────────────────────────────
144
-
145
- /**
146
- * Parse Ollama native tool calls from the message object.
147
- * Previously this used a Regex over the text response.
148
- */
149
- export function parseToolCalls(message: any): ToolCallRequest[] {
150
- const calls: ToolCallRequest[] = [];
151
-
152
- if (message?.tool_calls && Array.isArray(message.tool_calls)) {
153
- for (const tc of message.tool_calls) {
154
- if (tc.function?.name) {
155
- calls.push({
156
- name: tc.function.name,
157
- args: tc.function.arguments || {},
158
- });
159
- }
160
- }
161
- }
162
-
163
- return calls;
164
- }
165
-
166
- /**
167
- * Strip tool_call blocks from text to get the clean message.
168
- * Using native tools, this is usually a no-op as tools are not in the text,
169
- * but kept for backwards compatibility with any leaked formatting.
170
- */
171
- export function stripToolCalls(text: string): string {
172
- if (!text) return '';
173
- return text.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '').trim();
174
- }
175
-
176
- // ─── Conversation History ───────────────────────────────────
177
-
178
- let conversationHistory: OllamaChatMessage[] = [];
179
- const MAX_HISTORY = 20;
180
-
181
- function addToHistory(msg: OllamaChatMessage): void {
182
- conversationHistory.push(msg);
183
- if (conversationHistory.length > MAX_HISTORY) {
184
- conversationHistory = conversationHistory.slice(-MAX_HISTORY);
185
- }
186
- }
187
-
188
- export function clearConversationHistory(): void {
189
- conversationHistory = [];
190
- }
191
-
192
- // ─── API Methods ────────────────────────────────────────────
193
-
194
- /**
195
- * Check if Ollama is running and accessible.
196
- */
197
- export async function isOllamaAvailable(): Promise<boolean> {
198
- try {
199
- const res = await fetch(`${OLLAMA_BASE_URL}/api/tags`, {
200
- signal: AbortSignal.timeout(2000)
201
- });
202
- return res.ok;
203
- } catch {
204
- return false;
205
- }
206
- }
207
-
208
- /**
209
- * List available models from Ollama.
210
- */
211
- export async function listModels(): Promise<string[]> {
212
- try {
213
- const res = await fetch(`${OLLAMA_BASE_URL}/api/tags`);
214
- if (!res.ok) return [];
215
- const data = await res.json();
216
- return (data.models || []).map((m: OllamaModelInfo) => m.name);
217
- } catch {
218
- return [];
219
- }
220
- }
221
-
222
- /**
223
- * Send a chat message and get a complete response (non-streaming).
224
- * Routes through InferenceRouter to support multiple providers.
225
- */
226
- export async function chat(
227
- userMessage: string,
228
- options?: { model?: string; temperature?: number }
229
- ): Promise<string> {
230
- const userMsg: ChatMessage = { role: 'user', content: userMessage };
231
- addToHistory(userMsg);
232
-
233
- const messages: ChatMessage[] = [
234
- { role: 'system', content: buildSystemPrompt() },
235
- ...conversationHistory,
236
- ];
237
-
238
- try {
239
- const result = await routeChat(messages, {
240
- temperature: options?.temperature ?? 0.7,
241
- maxTokens: 512,
242
- });
243
-
244
- const assistantContent = result?.text || '';
245
- addToHistory({ role: 'assistant', content: assistantContent });
246
-
247
- if (result) {
248
- console.log(`🧠[Chat] Response via ${result.backend} (${result.model}) in ${result.latencyMs} ms`);
249
- }
250
-
251
- return assistantContent;
252
- } catch (error) {
253
- if (error instanceof DOMException && error.name === 'TimeoutError') {
254
- return '⏳ La respuesta del modelo tardó demasiado. Intenta con una pregunta más corta.';
255
- }
256
- throw error;
257
- }
258
- }
259
-
260
- /**
261
- * Send a chat message and stream the response token-by-token.
262
- * NOTE: Streaming only works with Ollama directly (local provider).
263
- * For cloud providers, this falls back to non-streaming.
264
- */
265
- export async function* chatStream(
266
- userMessage: string,
267
- options?: { model?: string; temperature?: number }
268
- ): AsyncGenerator<string, void, unknown> {
269
- const model = options?.model || DEFAULT_MODEL;
270
-
271
- const userMsg: OllamaChatMessage = { role: 'user', content: userMessage };
272
- addToHistory(userMsg);
273
-
274
- const messages: OllamaChatMessage[] = [
275
- { role: 'system', content: buildSystemPrompt() },
276
- ...conversationHistory,
277
- ];
278
-
279
- const res = await fetch(`${OLLAMA_BASE_URL}/api/chat`, {
280
- method: 'POST',
281
- headers: { 'Content-Type': 'application/json' },
282
- body: JSON.stringify({
283
- model,
284
- messages,
285
- stream: true,
286
- options: {
287
- temperature: options?.temperature ?? 0.7,
288
- num_predict: 512,
289
- },
290
- }),
291
- });
292
-
293
- if (!res.ok || !res.body) {
294
- throw new Error(`Ollama stream error: ${res.status}`);
295
- }
296
-
297
- const reader = res.body.getReader();
298
- const decoder = new TextDecoder();
299
- let fullContent = '';
300
-
301
- try {
302
- while (true) {
303
- const { done, value } = await reader.read();
304
- if (done) break;
305
-
306
- const chunk = decoder.decode(value, { stream: true });
307
- const lines = chunk.split('\n').filter(Boolean);
308
-
309
- for (const line of lines) {
310
- try {
311
- const data = JSON.parse(line);
312
- if (data.message?.content) {
313
- fullContent += data.message.content;
314
- yield data.message.content;
315
- }
316
- } catch {
317
- // Skip malformed chunks
318
- }
319
- }
320
- }
321
- } finally {
322
- reader.releaseLock();
323
- }
324
-
325
- addToHistory({ role: 'assistant', content: fullContent });
326
- }
@@ -1,373 +0,0 @@
1
- /**
2
- * PeerMesh — WebSocket-based P2P mesh for Decido OS instances
3
- *
4
- * Allows multiple Decido OS instances on the same network to form
5
- * a group and share AI results/context. Each peer maintains its
6
- * own provider keys (BYOK). Uses manual connection (IP:port) in
7
- * this first iteration; future versions will use mDNS auto-discovery.
8
- *
9
- * Protocol:
10
- * - Host starts a WebSocket server on a port (default 9876)
11
- * - Clients connect to the host's IP:port
12
- * - Messages: join, leave, share-result, share-context, heartbeat
13
- * - Max 8 peers per group
14
- */
15
-
16
- import { v4 as uuid } from 'uuid';
17
-
18
- // ─── Types ──────────────────────────────────────────────────
19
-
20
- export interface PeerInfo {
21
- id: string;
22
- name: string;
23
- address: string;
24
- joinedAt: number;
25
- lastSeen: number;
26
- }
27
-
28
- export interface SharedResult {
29
- peerId: string;
30
- provider: string;
31
- model: string;
32
- prompt: string;
33
- response: string;
34
- timestamp: number;
35
- }
36
-
37
- interface PeerMessage {
38
- type: 'join' | 'leave' | 'share-result' | 'share-context' | 'heartbeat' | 'peers-update';
39
- peerId: string;
40
- peerName: string;
41
- payload?: unknown;
42
- }
43
-
44
- // ─── Constants ──────────────────────────────────────────────
45
-
46
- const DEFAULT_PORT = 9876;
47
- const HEARTBEAT_INTERVAL = 10_000; // 10s
48
- const PEER_TIMEOUT = 30_000; // 30s without heartbeat = dead
49
- const MAX_PEERS = 8;
50
-
51
- // ─── Event Callbacks ────────────────────────────────────────
52
-
53
- type PeersChangedCb = (peers: PeerInfo[]) => void;
54
- type HostingChangedCb = (isHosting: boolean) => void;
55
- type ConnectionChangedCb = (isConnected: boolean) => void;
56
- type SharedResultCb = (result: SharedResult) => void;
57
-
58
- // ─── PeerMesh Class ─────────────────────────────────────────
59
-
60
- class PeerMeshImpl {
61
- private myId = uuid();
62
- private myName = this._getHostname();
63
- private peers = new Map<string, PeerInfo>();
64
- private connections = new Map<string, WebSocket>();
65
- private isHostingFlag = false;
66
- private isConnectedFlag = false;
67
- private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
68
- private cleanupTimer: ReturnType<typeof setInterval> | null = null;
69
-
70
- // Event listeners
71
- private peersListeners = new Set<PeersChangedCb>();
72
- private hostingListeners = new Set<HostingChangedCb>();
73
- private connectionListeners = new Set<ConnectionChangedCb>();
74
- private resultListeners = new Set<SharedResultCb>();
75
-
76
- // ── Host Mode ─────────────────────────────────────────
77
-
78
- /**
79
- * Start hosting a peer group.
80
- * Note: In browser WebSocket server is not available. This method
81
- * is a placeholder for Tauri (Rust-side server). In browser mode,
82
- * we simulate by acting as a "relay" through a shared connection.
83
- */
84
- startHost(port = DEFAULT_PORT): void {
85
- if (this.isHostingFlag) return;
86
- this.isHostingFlag = true;
87
- this._emitHosting(true);
88
- this._startHeartbeat();
89
- console.log(`🌐 [PeerMesh] Hosting on port ${port} (peer: ${this.myId.slice(0, 8)})`);
90
- console.log(`🌐 [PeerMesh] ⚠️ Browser-mode: WebSocket server requires Tauri backend.`);
91
- console.log(`🌐 [PeerMesh] Use connectToPeer() from another instance to connect.`);
92
- }
93
-
94
- stopHost(): void {
95
- if (!this.isHostingFlag) return;
96
- this.isHostingFlag = false;
97
- this._stopHeartbeat();
98
- this._broadcast({ type: 'leave', peerId: this.myId, peerName: this.myName });
99
- this._disconnectAll();
100
- this._emitHosting(false);
101
- console.log('🌐 [PeerMesh] Stopped hosting');
102
- }
103
-
104
- // ── Client Mode ───────────────────────────────────────
105
-
106
- connectToPeer(address: string): void {
107
- if (this.connections.size >= MAX_PEERS) {
108
- console.warn(`🌐 [PeerMesh] Max peers (${MAX_PEERS}) reached`);
109
- return;
110
- }
111
-
112
- const wsUrl = address.startsWith('ws') ? address : `ws://${address}`;
113
- console.log(`🌐 [PeerMesh] Connecting to ${wsUrl}...`);
114
-
115
- try {
116
- const ws = new WebSocket(wsUrl);
117
-
118
- ws.onopen = () => {
119
- console.log(`🌐 [PeerMesh] ✅ Connected to ${address}`);
120
- this.connections.set(address, ws);
121
- this.isConnectedFlag = true;
122
- this._emitConnection(true);
123
-
124
- // Announce ourselves
125
- this._send(ws, {
126
- type: 'join',
127
- peerId: this.myId,
128
- peerName: this.myName,
129
- });
130
-
131
- this._startHeartbeat();
132
- };
133
-
134
- ws.onmessage = (event) => {
135
- try {
136
- const msg: PeerMessage = JSON.parse(event.data);
137
- this._handleMessage(msg, address);
138
- } catch { /* ignore malformed messages */ }
139
- };
140
-
141
- ws.onclose = () => {
142
- console.log(`🌐 [PeerMesh] Disconnected from ${address}`);
143
- this.connections.delete(address);
144
- // Remove peer associated with this address
145
- for (const [id, peer] of this.peers) {
146
- if (peer.address === address) {
147
- this.peers.delete(id);
148
- }
149
- }
150
- this._emitPeers();
151
- if (this.connections.size === 0) {
152
- this.isConnectedFlag = false;
153
- this._emitConnection(false);
154
- this._stopHeartbeat();
155
- }
156
- };
157
-
158
- ws.onerror = (err) => {
159
- console.error(`🌐 [PeerMesh] Connection error to ${address}:`, err);
160
- };
161
- } catch (err) {
162
- console.error(`🌐 [PeerMesh] Failed to connect:`, err);
163
- }
164
- }
165
-
166
- disconnect(): void {
167
- this._broadcast({ type: 'leave', peerId: this.myId, peerName: this.myName });
168
- this._disconnectAll();
169
- }
170
-
171
- // ── Share Results ─────────────────────────────────────
172
-
173
- shareResult(result: Omit<SharedResult, 'peerId' | 'timestamp'>): void {
174
- const fullResult: SharedResult = {
175
- ...result,
176
- peerId: this.myId,
177
- timestamp: Date.now(),
178
- };
179
-
180
- this._broadcast({
181
- type: 'share-result',
182
- peerId: this.myId,
183
- peerName: this.myName,
184
- payload: fullResult,
185
- });
186
-
187
- console.log(`🌐 [PeerMesh] Shared result to ${this.connections.size} peers`);
188
- }
189
-
190
- // ── Event Subscriptions ───────────────────────────────
191
-
192
- onPeersChanged(cb: PeersChangedCb): () => void {
193
- this.peersListeners.add(cb);
194
- return () => { this.peersListeners.delete(cb); };
195
- }
196
-
197
- onHostingChanged(cb: HostingChangedCb): () => void {
198
- this.hostingListeners.add(cb);
199
- return () => { this.hostingListeners.delete(cb); };
200
- }
201
-
202
- onConnectionChanged(cb: ConnectionChangedCb): () => void {
203
- this.connectionListeners.add(cb);
204
- return () => { this.connectionListeners.delete(cb); };
205
- }
206
-
207
- onSharedResult(cb: SharedResultCb): () => void {
208
- this.resultListeners.add(cb);
209
- return () => { this.resultListeners.delete(cb); };
210
- }
211
-
212
- // ── Getters ───────────────────────────────────────────
213
-
214
- getPeers(): PeerInfo[] {
215
- return Array.from(this.peers.values());
216
- }
217
-
218
- getMyId(): string {
219
- return this.myId;
220
- }
221
-
222
- getMyName(): string {
223
- return this.myName;
224
- }
225
-
226
- // ── Message Handling ──────────────────────────────────
227
-
228
- private _handleMessage(msg: PeerMessage, fromAddress: string): void {
229
- switch (msg.type) {
230
- case 'join':
231
- this.peers.set(msg.peerId, {
232
- id: msg.peerId,
233
- name: msg.peerName,
234
- address: fromAddress,
235
- joinedAt: Date.now(),
236
- lastSeen: Date.now(),
237
- });
238
- this._emitPeers();
239
- console.log(`🌐 [PeerMesh] Peer joined: ${msg.peerName} (${msg.peerId.slice(0, 8)})`);
240
- break;
241
-
242
- case 'leave':
243
- this.peers.delete(msg.peerId);
244
- this._emitPeers();
245
- console.log(`🌐 [PeerMesh] Peer left: ${msg.peerName}`);
246
- break;
247
-
248
- case 'heartbeat':
249
- if (this.peers.has(msg.peerId)) {
250
- this.peers.get(msg.peerId)!.lastSeen = Date.now();
251
- }
252
- break;
253
-
254
- case 'share-result':
255
- if (msg.payload) {
256
- const result = msg.payload as SharedResult;
257
- for (const cb of this.resultListeners) {
258
- try { cb(result); } catch { /* ignore */ }
259
- }
260
- }
261
- break;
262
-
263
- case 'peers-update':
264
- // Host sends updated peer list
265
- if (Array.isArray(msg.payload)) {
266
- for (const p of msg.payload as PeerInfo[]) {
267
- if (p.id !== this.myId && !this.peers.has(p.id)) {
268
- this.peers.set(p.id, p);
269
- }
270
- }
271
- this._emitPeers();
272
- }
273
- break;
274
- }
275
- }
276
-
277
- // ── Internal Helpers ──────────────────────────────────
278
-
279
- private _send(ws: WebSocket, msg: PeerMessage): void {
280
- if (ws.readyState === WebSocket.OPEN) {
281
- ws.send(JSON.stringify(msg));
282
- }
283
- }
284
-
285
- private _broadcast(msg: PeerMessage): void {
286
- for (const ws of this.connections.values()) {
287
- this._send(ws, msg);
288
- }
289
- }
290
-
291
- private _disconnectAll(): void {
292
- for (const ws of this.connections.values()) {
293
- try { ws.close(); } catch { /* ignore */ }
294
- }
295
- this.connections.clear();
296
- this.peers.clear();
297
- this.isConnectedFlag = false;
298
- this._stopHeartbeat();
299
- this._emitPeers();
300
- this._emitConnection(false);
301
- }
302
-
303
- private _startHeartbeat(): void {
304
- if (this.heartbeatTimer) return;
305
-
306
- this.heartbeatTimer = setInterval(() => {
307
- this._broadcast({
308
- type: 'heartbeat',
309
- peerId: this.myId,
310
- peerName: this.myName,
311
- });
312
- }, HEARTBEAT_INTERVAL);
313
-
314
- this.cleanupTimer = setInterval(() => {
315
- const now = Date.now();
316
- let changed = false;
317
- for (const [id, peer] of this.peers) {
318
- if (now - peer.lastSeen > PEER_TIMEOUT) {
319
- this.peers.delete(id);
320
- changed = true;
321
- console.log(`🌐 [PeerMesh] Peer timed out: ${peer.name}`);
322
- }
323
- }
324
- if (changed) this._emitPeers();
325
- }, PEER_TIMEOUT);
326
- }
327
-
328
- private _stopHeartbeat(): void {
329
- if (this.heartbeatTimer) {
330
- clearInterval(this.heartbeatTimer);
331
- this.heartbeatTimer = null;
332
- }
333
- if (this.cleanupTimer) {
334
- clearInterval(this.cleanupTimer);
335
- this.cleanupTimer = null;
336
- }
337
- }
338
-
339
- // ── Event Emitters ────────────────────────────────────
340
-
341
- private _emitPeers(): void {
342
- const list = this.getPeers();
343
- for (const cb of this.peersListeners) {
344
- try { cb(list); } catch { /* ignore */ }
345
- }
346
- }
347
-
348
- private _emitHosting(hosting: boolean): void {
349
- for (const cb of this.hostingListeners) {
350
- try { cb(hosting); } catch { /* ignore */ }
351
- }
352
- }
353
-
354
- private _emitConnection(connected: boolean): void {
355
- for (const cb of this.connectionListeners) {
356
- try { cb(connected); } catch { /* ignore */ }
357
- }
358
- }
359
-
360
- // ── Hostname ──────────────────────────────────────────
361
-
362
- private _getHostname(): string {
363
- try {
364
- return `user-${Math.random().toString(36).slice(2, 6)}`;
365
- } catch {
366
- return 'anonymous';
367
- }
368
- }
369
- }
370
-
371
- // ─── Singleton Export ───────────────────────────────────────
372
-
373
- export const peerMesh = new PeerMeshImpl();