This commit is contained in:
381
scripts/seed-real-data.mjs
Normal file
381
scripts/seed-real-data.mjs
Normal file
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Real-Data Seed Script — populates personal.mind with Marko's actual context.
|
||||
*
|
||||
* This is NOT test data. This is real information about:
|
||||
* - Marko Markovic (identity, role, preferences)
|
||||
* - Waggle OS (product, architecture, decisions)
|
||||
* - KVARK (enterprise AI platform)
|
||||
* - Egzakta Group (parent company)
|
||||
* - Technology stack, business strategy, team context
|
||||
*
|
||||
* Run: node scripts/seed-real-data.mjs
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import fs from 'node:fs';
|
||||
import {
|
||||
MindDB,
|
||||
FrameStore,
|
||||
KnowledgeGraph,
|
||||
IdentityLayer,
|
||||
AwarenessLayer,
|
||||
SessionStore,
|
||||
HybridSearch,
|
||||
WorkspaceManager,
|
||||
createEmbeddingProvider,
|
||||
} from '@waggle/core';
|
||||
import { CompilationState } from '@waggle/wiki-compiler';
|
||||
|
||||
// ── Setup ─────────────────────────────────────────────────────────
|
||||
|
||||
const dataDir = process.env.WAGGLE_DATA_DIR
|
||||
? process.env.WAGGLE_DATA_DIR.replace('~', os.homedir())
|
||||
: path.join(os.homedir(), '.waggle');
|
||||
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
|
||||
const mindPath = path.join(dataDir, 'personal.mind');
|
||||
console.log(`\n📂 Data directory: ${dataDir}`);
|
||||
console.log(`🧠 Mind path: ${mindPath}`);
|
||||
|
||||
const db = new MindDB(mindPath);
|
||||
const frameStore = new FrameStore(db);
|
||||
const kg = new KnowledgeGraph(db);
|
||||
const identity = new IdentityLayer(db);
|
||||
const awareness = new AwarenessLayer(db);
|
||||
const sessions = new SessionStore(db);
|
||||
const wsManager = new WorkspaceManager(dataDir);
|
||||
|
||||
// Use real embeddings if available: inprocess (local) > ollama > mock
|
||||
const embeddingProvider = process.env.WAGGLE_EMBEDDING_PROVIDER ?? 'inprocess';
|
||||
console.log(`📐 Embedder: ${embeddingProvider}`);
|
||||
const embedder = await createEmbeddingProvider({
|
||||
provider: embeddingProvider,
|
||||
inprocess: { cacheDir: path.join(dataDir, 'models') },
|
||||
});
|
||||
const search = new HybridSearch(db, embedder);
|
||||
|
||||
// ── Step 1: Wipe test pollution ───────────────────────────────────
|
||||
|
||||
console.log('\n🧹 Step 1: Wiping test pollution...');
|
||||
|
||||
const raw = db.getDatabase();
|
||||
const importCount = raw.prepare("SELECT COUNT(*) as cnt FROM memory_frames WHERE source = 'import'").get();
|
||||
console.log(` Import frames to delete: ${importCount.cnt}`);
|
||||
|
||||
// Delete import frames using FrameStore.delete() which handles FK cascades
|
||||
const importIds = raw.prepare("SELECT id FROM memory_frames WHERE source = 'import'").all();
|
||||
for (const { id } of importIds) {
|
||||
frameStore.delete(id);
|
||||
}
|
||||
|
||||
// Retire noise entities
|
||||
const allEntities = kg.getEntities(10000);
|
||||
const noisePatterns = /^(step|phase|part|begin|end|test|true|false|null|yes|no|ok|the |a |an )/i;
|
||||
let noiseRetired = 0;
|
||||
const noiseTx = raw.transaction(() => {
|
||||
for (const e of allEntities) {
|
||||
if (e.name.length <= 2 || /^\d+$/.test(e.name) || noisePatterns.test(e.name)) {
|
||||
kg.retireEntity(e.id);
|
||||
noiseRetired++;
|
||||
}
|
||||
}
|
||||
});
|
||||
noiseTx();
|
||||
console.log(` Noise entities retired: ${noiseRetired}`);
|
||||
|
||||
// Clean wiki compilation state if exists
|
||||
try {
|
||||
raw.prepare('DELETE FROM wiki_pages').run();
|
||||
raw.prepare('DELETE FROM wiki_watermark').run();
|
||||
} catch { /* tables may not exist yet */ }
|
||||
|
||||
const statsAfter = frameStore.getStats();
|
||||
console.log(` Frames remaining: ${statsAfter.total}`);
|
||||
|
||||
// ── Step 2: Set identity ──────────────────────────────────────────
|
||||
|
||||
console.log('\n👤 Step 2: Setting Marko\'s identity...');
|
||||
|
||||
try {
|
||||
if (identity.exists()) {
|
||||
identity.update({
|
||||
name: 'Marko Markovic',
|
||||
role: 'CEO & Technical Co-founder',
|
||||
department: 'Executive / Engineering',
|
||||
personality: 'Direct, strategic, moves fast. Prefers concise communication. Values shipping over perfection. Thinks in systems and business models.',
|
||||
capabilities: 'Full-stack development, AI/ML architecture, product strategy, enterprise sales, team leadership. Deep expertise in TypeScript, React, Node.js, Tauri, SQLite. Building AI-native products.',
|
||||
system_prompt: 'Marko is building Waggle OS (AI workspace platform) and KVARK (enterprise sovereign AI) at Egzakta Group. He values speed, real results, and hates unnecessary process. Help him ship.',
|
||||
});
|
||||
} else {
|
||||
identity.create({
|
||||
name: 'Marko Markovic',
|
||||
role: 'CEO & Technical Co-founder',
|
||||
department: 'Executive / Engineering',
|
||||
personality: 'Direct, strategic, moves fast. Prefers concise communication. Values shipping over perfection. Thinks in systems and business models.',
|
||||
capabilities: 'Full-stack development, AI/ML architecture, product strategy, enterprise sales, team leadership. Deep expertise in TypeScript, React, Node.js, Tauri, SQLite. Building AI-native products.',
|
||||
system_prompt: 'Marko is building Waggle OS (AI workspace platform) and KVARK (enterprise sovereign AI) at Egzakta Group. He values speed, real results, and hates unnecessary process. Help him ship.',
|
||||
});
|
||||
}
|
||||
console.log(' ✅ Identity configured');
|
||||
} catch (err) {
|
||||
console.log(` ⚠️ Identity: ${err.message}`);
|
||||
}
|
||||
|
||||
// ── Step 3: Create workspaces ─────────────────────────────────────
|
||||
|
||||
console.log('\n🏗️ Step 3: Creating workspaces...');
|
||||
|
||||
const workspaces = [
|
||||
{ name: 'Waggle OS', group: 'products', icon: '🐝' },
|
||||
{ name: 'KVARK', group: 'products', icon: '⚛️' },
|
||||
{ name: 'Egzakta Group', group: 'company', icon: '🏢' },
|
||||
{ name: 'AI Research', group: 'research', icon: '🔬' },
|
||||
{ name: 'Sales Pipeline', group: 'business', icon: '💼' },
|
||||
];
|
||||
|
||||
for (const ws of workspaces) {
|
||||
try {
|
||||
const existing = wsManager.list().find(w => w.name === ws.name);
|
||||
if (!existing) {
|
||||
wsManager.create({ name: ws.name, group: ws.group });
|
||||
console.log(` ✅ Created workspace: ${ws.name}`);
|
||||
} else {
|
||||
console.log(` ⏭️ Workspace exists: ${ws.name}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(` ⚠️ ${ws.name}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 4: Save real memories ────────────────────────────────────
|
||||
|
||||
console.log('\n💾 Step 4: Saving real memories...');
|
||||
|
||||
const realMemories = [
|
||||
// ── Waggle OS Architecture ──
|
||||
{
|
||||
session: 'waggle-architecture',
|
||||
summary: 'Waggle OS architecture decisions',
|
||||
frames: [
|
||||
{ content: 'Waggle OS is a workspace-native AI agent platform with persistent memory. Ships as a Tauri 2.0 desktop binary for Windows and macOS with a React frontend and a Node.js sidecar.', importance: 'critical' },
|
||||
{ content: 'The memory engine uses SQLite with FrameStore (I/P/B frames), HybridSearch (FTS5 + sqlite-vec), and KnowledgeGraph (entity-relation). All stored in a single .mind file per workspace.', importance: 'critical' },
|
||||
{ content: 'Frontend is React 18 + TypeScript + Vite + Tailwind + shadcn/ui. Desktop shell is Tauri 2.0 (Rust). Backend is Fastify sidecar (Node.js, bundled). Design system is Hive DS with honey #e5a000 accent.', importance: 'important' },
|
||||
{ content: 'Agent runtime at packages/agent/src/agent-loop.ts. 22 personas available. Behavioral spec v2.0 governs agent behavior. Tool filtering per persona via allowlist/denylist.', importance: 'important' },
|
||||
{ content: 'Authentication via Clerk (JWT-based). Tier system: Trial (15d) → Free → Pro ($19/mo) → Teams ($49/seat/mo) → Enterprise (KVARK). Memory + Harvest free forever as moat.', importance: 'critical' },
|
||||
{ content: 'The Room is the desktop canvas — multiple chat windows, per-window personas, workspace rail, cross-workspace tools. Autonomy levels: Normal, Trusted, YOLO.', importance: 'important' },
|
||||
{ content: 'Memory MCP plugin at packages/memory-mcp/ — 18 MCP tools + 4 resources. Works with Claude Code, Claude Desktop, and any MCP-compatible AI. Data stored in ~/.waggle/.', importance: 'important' },
|
||||
],
|
||||
},
|
||||
|
||||
// ── KVARK Strategy ──
|
||||
{
|
||||
session: 'kvark-strategy',
|
||||
summary: 'KVARK enterprise AI platform strategy',
|
||||
frames: [
|
||||
{ content: 'KVARK is Egzakta Group\'s sovereign enterprise AI platform. Everything Waggle does — on your infrastructure, connected to all your internal systems. Full data pipeline injection, your permissions, complete audit trail, governance. Your data never leaves your perimeter.', importance: 'critical' },
|
||||
{ content: 'KVARK has EUR 1.2M in contracted revenue. Waggle OS is the demand-generation and qualification engine for KVARK — solo users learn what AI-native work feels like, then enterprises want it on their infrastructure.', importance: 'critical' },
|
||||
{ content: 'Waggle tier funnel: Solo (Free) teaches individuals → Basic ($15/mo) removes limits → Teams ($79/mo) creates institutional dependency → Enterprise leads to KVARK consultative sale at www.kvark.ai.', importance: 'important' },
|
||||
{ content: 'KVARK differentiators: sovereign deployment (on-prem or private cloud), full Microsoft 365 integration, EU AI Act compliance by default, enterprise governance, custom model pools, audit trail.', importance: 'important' },
|
||||
],
|
||||
},
|
||||
|
||||
// ── Egzakta Group ──
|
||||
{
|
||||
session: 'egzakta-company',
|
||||
summary: 'Egzakta Group company context',
|
||||
frames: [
|
||||
{ content: 'Egzakta Group is the parent company building Waggle OS and KVARK. Founded by Marko Markovic. Focus on enterprise AI solutions with a sovereign-first approach.', importance: 'important' },
|
||||
{ content: 'LM TEK is the hardware arm — provides GPU infrastructure for KVARK deployments. Enables fully on-premises AI without cloud dependencies.', importance: 'normal' },
|
||||
{ content: 'Business model: Waggle OS is freemium SaaS (demand gen) → KVARK is enterprise consultative sale (EUR 1.2M contracted). Memory Harvest is the free moat that locks users in.', importance: 'important' },
|
||||
],
|
||||
},
|
||||
|
||||
// ── Technical Decisions ──
|
||||
{
|
||||
session: 'tech-decisions',
|
||||
summary: 'Key technology decisions and rationale',
|
||||
frames: [
|
||||
{ content: 'Decision: Use SQLite + sqlite-vec for embeddings instead of a separate vector database. Rationale: single-file portability (.mind file), no infrastructure dependencies, good enough for personal/workspace scale.', importance: 'important' },
|
||||
{ content: 'Decision: Tauri 2.0 over Electron. Rationale: 10x smaller binary, Rust security, native performance. Trade-off: harder to debug, less ecosystem.', importance: 'important' },
|
||||
{ content: 'Decision: Ship Memory MCP as standalone npm package. Rationale: works with ANY MCP client (Claude Code, Claude Desktop, Cursor, etc.), not just Waggle. Expands TAM massively.', importance: 'critical' },
|
||||
{ content: 'Decision: Wiki Compiler uses Karpathy-style LLM wiki approach. Memory frames are raw material, LLM synthesizes them into interlinked markdown wiki pages. Incremental compilation via watermarks.', importance: 'critical' },
|
||||
{ content: 'Decision: EU AI Act compliance baked into memory system. Art. 12/13/14/19/26/50 mapped to specific features. Compliance by default, not as afterthought. Aug 2 2026 deadline.', importance: 'important' },
|
||||
{ content: 'Decision: All tiers get unlimited embedding quotas. Previous per-tier quotas removed during tier restructure. Memory + Harvest are free forever — they are the moat.', importance: 'normal' },
|
||||
],
|
||||
},
|
||||
|
||||
// ── Wiki Compiler ──
|
||||
{
|
||||
session: 'wiki-compiler',
|
||||
summary: 'Wiki Compiler concept and implementation',
|
||||
frames: [
|
||||
{ content: 'Wiki Compiler is inspired by Karpathy\'s LLM Wiki concept — using LLMs to incrementally build and maintain a persistent, interlinked markdown wiki from accumulated knowledge. The key insight: answers should compound into permanent knowledge, not disappear with the chat.', importance: 'critical' },
|
||||
{ content: 'Competitive landscape analyzed: Google Brain markdown+PGLite, Mem0 facts graph, Zep temporal, Hindsight auto-capture, Cognee scientific. Waggle Wiki Compiler combines best of all with source provenance and incremental compilation.', importance: 'important' },
|
||||
{ content: 'Wiki page types: entity (person/project/org), concept (topic synthesis), synthesis (cross-source patterns — the killer feature), index (catalog), health (contradictions/gaps/orphans).', importance: 'important' },
|
||||
{ content: 'Universal source pipeline: 30+ adapters in 3 tiers. Phase 1 ships with markdown, plaintext, PDF, URL adapters plus existing ChatGPT/Claude/Gemini/ClaudeCode harvest adapters.', importance: 'normal' },
|
||||
{ content: 'Privacy architecture: local-first processing, PII filtering (redact/flag/pass/ask modes), GDPR data portability (single .mind file export), zero telemetry by default, end-to-end encryption for team sync.', importance: 'important' },
|
||||
],
|
||||
},
|
||||
|
||||
// ── Development Velocity ──
|
||||
{
|
||||
session: 'dev-velocity',
|
||||
summary: 'Development progress and velocity',
|
||||
frames: [
|
||||
{ content: 'Waggle OS development velocity: 29 commits in a single MEGA session, entire backlog cleared. Team memory, S3 storage, global KG, trial modal, budget cap, Ollama integration, 13k dead code removed.', importance: 'normal' },
|
||||
{ content: 'Phase A (The Room) + Phase B (Real Filesystem + Tiered Autonomy) completed in approximately one working day. Per-session orchestrators, per-window personas, Room canvas, window restoration, workspace rail.', importance: 'normal' },
|
||||
{ content: 'E2E test suite: 96 Playwright tests covering all critical user flows. API tests + visual baseline tests. Onboarding skip via addInitScript before goto.', importance: 'normal' },
|
||||
{ content: 'Memory MCP plugin has 18 MCP tools and 4 resources. Handles persistent memory, knowledge graph, identity, awareness, workspace management, harvest, cleanup, ingestion, and wiki compilation.', importance: 'normal' },
|
||||
],
|
||||
},
|
||||
|
||||
// ── Preferences & Working Style ──
|
||||
{
|
||||
session: 'working-style',
|
||||
summary: 'Marko\'s working style and preferences',
|
||||
frames: [
|
||||
{ content: 'Marko prefers terse, action-oriented communication. No summaries of what was just done — he can read the diff. Lead with Playwright E2E testing, don\'t make him click through the UI manually.', importance: 'important' },
|
||||
{ content: 'When Marko says "you decide" or "you lead" — take initiative, make decisions, keep moving. Don\'t ask for permission on obvious next steps. Ship first, polish later.', importance: 'important' },
|
||||
{ content: 'Marko values real integration tests over mocked tests. Got burned when mock tests passed but production migration failed. Use real database connections in tests.', importance: 'normal' },
|
||||
{ content: 'For refactors, Marko prefers one bundled PR over many small ones. Splitting certain changes creates unnecessary churn. Use judgment on when to bundle vs split.', importance: 'normal' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
let totalFrames = 0;
|
||||
for (const mem of realMemories) {
|
||||
const session = sessions.ensure(
|
||||
`seed:${mem.session}`,
|
||||
undefined,
|
||||
mem.summary,
|
||||
);
|
||||
|
||||
for (const frame of mem.frames) {
|
||||
const existing = frameStore.findDuplicate(frame.content);
|
||||
if (!existing) {
|
||||
const f = frameStore.createIFrame(session.gop_id, frame.content, frame.importance, 'user_stated');
|
||||
try { await search.indexFrame(f.id, frame.content); } catch {}
|
||||
totalFrames++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` ✅ Saved ${totalFrames} real memory frames across ${realMemories.length} sessions`);
|
||||
|
||||
// ── Step 5: Build KG entities & relations ─────────────────────────
|
||||
|
||||
console.log('\n🕸️ Step 5: Building knowledge graph...');
|
||||
|
||||
// Helper to create entity if not exists
|
||||
function ensureEntity(type, name, properties = {}) {
|
||||
const existing = kg.searchEntities(name, 1);
|
||||
const match = existing.find(e => e.name.toLowerCase() === name.toLowerCase() && e.entity_type === type);
|
||||
if (match) return match;
|
||||
return kg.createEntity(type, name, properties);
|
||||
}
|
||||
|
||||
// People
|
||||
const marko = ensureEntity('person', 'Marko Markovic', { role: 'CEO & Technical Co-founder', company: 'Egzakta Group' });
|
||||
|
||||
// Organizations
|
||||
const egzakta = ensureEntity('organization', 'Egzakta Group', { type: 'parent company', focus: 'enterprise AI' });
|
||||
const lmtek = ensureEntity('organization', 'LM TEK', { type: 'hardware division', focus: 'GPU infrastructure' });
|
||||
|
||||
// Projects
|
||||
const waggle = ensureEntity('project', 'Waggle OS', { type: 'AI workspace platform', stage: 'active development' });
|
||||
const kvark = ensureEntity('project', 'KVARK', { type: 'enterprise sovereign AI', revenue: 'EUR 1.2M contracted' });
|
||||
const wikiCompiler = ensureEntity('project', 'Wiki Compiler', { type: 'knowledge synthesis engine', status: 'v1 built' });
|
||||
const memoryMcp = ensureEntity('project', 'Memory MCP', { type: 'MCP plugin', tools: 18 });
|
||||
|
||||
// Technologies
|
||||
const react = ensureEntity('technology', 'React', { version: '18', usage: 'frontend' });
|
||||
const typescript = ensureEntity('technology', 'TypeScript', { usage: 'primary language' });
|
||||
const tauri = ensureEntity('technology', 'Tauri', { version: '2.0', usage: 'desktop shell' });
|
||||
const sqlite = ensureEntity('technology', 'SQLite', { usage: 'memory storage, .mind files' });
|
||||
const fastify = ensureEntity('technology', 'Fastify', { usage: 'sidecar API server' });
|
||||
const clerk = ensureEntity('technology', 'Clerk', { usage: 'authentication (JWT)' });
|
||||
const stripe = ensureEntity('technology', 'Stripe', { usage: 'payments (pending)' });
|
||||
const mcp = ensureEntity('technology', 'MCP Protocol', { usage: 'tool interop standard' });
|
||||
|
||||
// Concepts
|
||||
const hiveMind = ensureEntity('concept', 'Hive Mind', { description: 'Personal wiki compiled from memory frames' });
|
||||
const memoryHarvest = ensureEntity('concept', 'Memory Harvest', { description: 'Import from external AI systems' });
|
||||
const aiAct = ensureEntity('concept', 'EU AI Act', { description: 'Compliance framework, Aug 2026 deadline' });
|
||||
const tierStrategy = ensureEntity('concept', 'Tier Strategy', { description: 'Trial→Free→Pro→Teams→Enterprise→KVARK' });
|
||||
const sovereignty = ensureEntity('concept', 'Data Sovereignty', { description: 'Customer data never leaves their perimeter' });
|
||||
|
||||
console.log(` ✅ Created/verified ${15 + 5} entities`);
|
||||
|
||||
// Relations
|
||||
const relations = [
|
||||
// Marko
|
||||
[marko.id, egzakta.id, 'founded', 1.0],
|
||||
[marko.id, waggle.id, 'leads', 1.0],
|
||||
[marko.id, kvark.id, 'leads', 1.0],
|
||||
|
||||
// Company structure
|
||||
[egzakta.id, waggle.id, 'builds', 1.0],
|
||||
[egzakta.id, kvark.id, 'builds', 1.0],
|
||||
[egzakta.id, lmtek.id, 'owns', 0.9],
|
||||
|
||||
// Product relationships
|
||||
[waggle.id, kvark.id, 'feeds_demand_to', 1.0],
|
||||
[waggle.id, memoryMcp.id, 'includes', 1.0],
|
||||
[waggle.id, wikiCompiler.id, 'includes', 1.0],
|
||||
[kvark.id, sovereignty.id, 'implements', 1.0],
|
||||
|
||||
// Tech stack
|
||||
[waggle.id, react.id, 'uses', 1.0],
|
||||
[waggle.id, typescript.id, 'uses', 1.0],
|
||||
[waggle.id, tauri.id, 'uses', 1.0],
|
||||
[waggle.id, sqlite.id, 'uses', 1.0],
|
||||
[waggle.id, fastify.id, 'uses', 1.0],
|
||||
[waggle.id, clerk.id, 'uses', 0.9],
|
||||
[waggle.id, stripe.id, 'will_use', 0.7],
|
||||
[memoryMcp.id, mcp.id, 'implements', 1.0],
|
||||
|
||||
// Concepts
|
||||
[wikiCompiler.id, hiveMind.id, 'implements', 1.0],
|
||||
[waggle.id, memoryHarvest.id, 'provides', 1.0],
|
||||
[waggle.id, aiAct.id, 'complies_with', 0.8],
|
||||
[waggle.id, tierStrategy.id, 'follows', 1.0],
|
||||
];
|
||||
|
||||
let relationsCreated = 0;
|
||||
for (const [srcId, tgtId, type, confidence] of relations) {
|
||||
try {
|
||||
kg.createRelation(srcId, tgtId, type, confidence);
|
||||
relationsCreated++;
|
||||
} catch { /* may already exist */ }
|
||||
}
|
||||
|
||||
console.log(` ✅ Created ${relationsCreated} relations`);
|
||||
|
||||
// ── Step 6: Summary ───────────────────────────────────────────────
|
||||
|
||||
const finalStats = frameStore.getStats();
|
||||
const entityCount = kg.getEntityCount();
|
||||
|
||||
console.log('\n📊 Final State:');
|
||||
console.log(` Frames: ${finalStats.total} (by type: I=${finalStats.byType['I'] ?? 0}, P=${finalStats.byType['P'] ?? 0})`);
|
||||
console.log(` Entities: ${entityCount}`);
|
||||
console.log(` Workspaces: ${wsManager.list().length}`);
|
||||
console.log(` Identity: ${identity.exists() ? '✅ configured' : '❌ missing'}`);
|
||||
|
||||
// Verify wiki tables exist
|
||||
try {
|
||||
const compilationState = new CompilationState(db);
|
||||
const wm = compilationState.getWatermark();
|
||||
console.log(` Wiki watermark: frame #${wm.lastFrameId}`);
|
||||
} catch (err) {
|
||||
console.log(` Wiki state: not initialized yet`);
|
||||
}
|
||||
|
||||
console.log('\n✅ Real data seeded successfully!');
|
||||
console.log(' Next: harvest Claude Code memories, then compile wiki.\n');
|
||||
|
||||
db.close();
|
||||
Reference in New Issue
Block a user