fix(ai): revert thinkingBudget to thinkingBudgetTokens
This commit is contained in:
@@ -1,48 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
const file = 'vibn-frontend/app/api/mcp/route.ts';
|
|
||||||
let code = fs.readFileSync(file, 'utf8');
|
|
||||||
|
|
||||||
// 1. Fix toolFsWrite
|
|
||||||
const oldFsWriteReturn = ` return NextResponse.json({
|
|
||||||
result: { path, bytesWritten: Buffer.byteLength(content, "utf8") },
|
|
||||||
});`;
|
|
||||||
|
|
||||||
const newFsWriteReturn = ` const { createHash } = require('crypto');
|
|
||||||
const bytes = Buffer.byteLength(content, "utf8");
|
|
||||||
const sha256 = createHash("sha256").update(content, "utf8").digest("hex");
|
|
||||||
return NextResponse.json({
|
|
||||||
result: { ok: true, path, bytes, sha256 },
|
|
||||||
});`;
|
|
||||||
|
|
||||||
code = code.replace(oldFsWriteReturn, newFsWriteReturn);
|
|
||||||
|
|
||||||
|
|
||||||
// 2. Fix toolFsEdit
|
|
||||||
const oldFsEditCmd = `const cmd = \`python3 -c "$(printf %s \${shq(pyB64)} | base64 -d)" <<< "$(printf %s \${shq(b64)} | base64 -d)"\`;`;
|
|
||||||
const newFsEditCmd = `const cmd = \`python3 -c "$(printf %s \${shq(pyB64)} | base64 -d)" <<< "$(printf %s \${shq(b64)} | base64 -d)" && echo "---" && sha256sum \${shq(path)} | cut -d' ' -f1 && wc -c < \${shq(path)}\`;`;
|
|
||||||
|
|
||||||
code = code.replace(oldFsEditCmd, newFsEditCmd);
|
|
||||||
|
|
||||||
const oldFsEditReturn = ` return NextResponse.json({
|
|
||||||
result: { path, replacements: parseInt(r.stdout.trim() || "0", 10) },
|
|
||||||
});`;
|
|
||||||
|
|
||||||
const newFsEditReturn = ` const stdoutParts = r.stdout.split('---');
|
|
||||||
const replacementsStr = stdoutParts[0].trim();
|
|
||||||
const hashAndSize = stdoutParts[1] ? stdoutParts[1].trim().split('\\n') : [];
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
result: {
|
|
||||||
ok: true,
|
|
||||||
path,
|
|
||||||
replacements: parseInt(replacementsStr || "0", 10),
|
|
||||||
sha256: hashAndSize[0] ? hashAndSize[0].trim() : undefined,
|
|
||||||
bytes: hashAndSize[1] ? parseInt(hashAndSize[1].trim(), 10) : undefined
|
|
||||||
},
|
|
||||||
});`;
|
|
||||||
|
|
||||||
code = code.replace(oldFsEditReturn, newFsEditReturn);
|
|
||||||
|
|
||||||
fs.writeFileSync(file, code);
|
|
||||||
console.log("Patched toolFsWrite and toolFsEdit");
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
const file = 'vibn-frontend/app/api/mcp/route.ts';
|
|
||||||
let code = fs.readFileSync(file, 'utf8');
|
|
||||||
|
|
||||||
const oldNormalize = `function normalizeFsPath(p: string): string | NextResponse {
|
|
||||||
if (!p || typeof p !== "string") {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Param "path" is required' },
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let abs: string;
|
|
||||||
if (p.startsWith("/")) {
|
|
||||||
abs = p;
|
|
||||||
} else {
|
|
||||||
abs = \`\${FS_ROOT}/\${p}\`.replace(/\\/+/g, "/");
|
|
||||||
}
|
|
||||||
// Disallow .. traversal that escapes /workspace.
|
|
||||||
const norm = abs.replace(/\\/[^/]+\\/\\.\\.(?=\\/|$)/g, "").replace(/\\/+/g, "/");
|
|
||||||
if (!norm.startsWith(FS_ROOT) && norm !== FS_ROOT) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: \`Path "\${p}" is outside \${FS_ROOT}; use shell.exec for system paths.\`,
|
|
||||||
},
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return norm;
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const newNormalize = `function normalizeFsPath(
|
|
||||||
p: string,
|
|
||||||
projectSlug?: string,
|
|
||||||
): string | NextResponse {
|
|
||||||
if (!p || typeof p !== "string") {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Param "path" is required' },
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const projectRoot = projectSlug ? \`\${FS_ROOT}/\${projectSlug}\` : FS_ROOT;
|
|
||||||
let abs: string;
|
|
||||||
if (p.startsWith("/")) {
|
|
||||||
abs = p;
|
|
||||||
} else {
|
|
||||||
abs = \`\${projectRoot}/\${p}\`.replace(/\\/+/g, "/");
|
|
||||||
}
|
|
||||||
const norm = abs.replace(/\\/[^/]+\\/\\.\\.(?=\\/|$)/g, "").replace(/\\/+/g, "/");
|
|
||||||
|
|
||||||
// When projectSlug is set, REJECT paths outside the project root.
|
|
||||||
if (projectSlug) {
|
|
||||||
if (!norm.startsWith(projectRoot) && norm !== projectRoot) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
ok: false,
|
|
||||||
error: \`PATH_OUTSIDE_PROJECT: path "\${p}" resolves to "\${norm}" which is outside the active project at "\${projectRoot}". Did you mean "\${projectRoot}/\${p.replace(/^\\/+/, "")}"?\`,
|
|
||||||
},
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Workspace-level fallback (legacy behaviour)
|
|
||||||
if (!norm.startsWith(FS_ROOT) && norm !== FS_ROOT) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: \`Path "\${p}" is outside \${FS_ROOT}; use shell.exec for system paths.\` },
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return norm;
|
|
||||||
}`;
|
|
||||||
|
|
||||||
code = code.replace(oldNormalize, newNormalize);
|
|
||||||
|
|
||||||
code = code.replace(/const path = normalizeFsPath\(String\(params\.path \?\? ""\)\);/g, 'const path = normalizeFsPath(String(params.path ?? ""), project.slug);');
|
|
||||||
code = code.replace(/const path = normalizeFsPath\(String\(params\.path \?\? "\/workspace"\)\);/g, 'const path = normalizeFsPath(String(params.path ?? "/workspace"), project.slug);');
|
|
||||||
code = code.replace(/const cwd = normalizeFsPath\(String\(params\.cwd \?\? "\/workspace"\)\);/g, 'const cwd = normalizeFsPath(String(params.cwd ?? "/workspace"), project.slug);');
|
|
||||||
code = code.replace(/const targetPath = normalizeFsPath\(String\(params\.targetPath \?\? ""\)\);/g, 'const targetPath = normalizeFsPath(String(params.targetPath ?? ""), project.slug);');
|
|
||||||
|
|
||||||
fs.writeFileSync(file, code);
|
|
||||||
console.log("Patched normalizeFsPath with projectSlug scoping");
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
const file = 'vibn-frontend/app/api/chat/route.ts';
|
|
||||||
let code = fs.readFileSync(file, 'utf8');
|
|
||||||
|
|
||||||
// Fix 4: apps_containers_list -> apps_containers_ps
|
|
||||||
code = code.replace(/apps_containers_list \{ uuid \}/g, 'apps_containers_ps { uuid }');
|
|
||||||
|
|
||||||
// Fix 5: Soften ok field rule
|
|
||||||
const oldOkRule = `- **Trust the \\\`ok\\\` field.** Every tool result carries \\\`ok: true | false\\\`. If \\\`ok\\\` is false (or \\\`exitCode\\\` is non-zero, or \\\`healthCheck.status\\\` is >= 400), the operation FAILED. Do not describe a failed operation as successful. Surface the error verbatim and propose a next step.`;
|
|
||||||
const newOkRule = `- **Read tool results carefully.** A tool FAILED when ANY of these signals are present: \\\`ok: false\\\`, \\\`error: "..."\\\`, a non-zero \\\`exitCode\\\`, or a \\\`healthCheck.status\\\` >= 400. If NONE of those signals are present, look at the actual content of the response to decide whether the operation succeeded. Many read-only tools return data directly without an \\\`ok\\\` field — that's not a failure.`;
|
|
||||||
code = code.replace(oldOkRule, newOkRule);
|
|
||||||
|
|
||||||
// Fix 7: Path conventions
|
|
||||||
const oldDirRule = `- **Directory:** the tool resolves paths relative to the active project root — you can pass \\\`command: "npm run dev"\\\` directly. (If you need to manually \\\`cd\\\`, use the project slug.)`;
|
|
||||||
const newDirRule = `- **Directory:** Tool paths are scoped to your project root automatically. Pass \\\`command: "npm run dev"\\\` directly — no \\\`cd\\\` prefix needed. The tool rejects any \\\`fs_*\\\` write outside \\\`/workspace/<slug>/\\\`.`;
|
|
||||||
code = code.replace(oldDirRule, newDirRule);
|
|
||||||
|
|
||||||
const oldPathConv = `**fs_* path convention for this project:** Pass paths relative to \\\`/workspace/\${activeProject.slug ?? "<slug>"}/\\\` — e.g. \\\`src/app/page.tsx\\\`, not \\\`/workspace/\${activeProject.slug ?? "<slug>"}/src/app/page.tsx\\\` and not \\\`getacquired-style/src/app/page.tsx\\\`. The tool layer rejects paths outside the project root.`;
|
|
||||||
const newPathConv = `**Path convention for fs_* tools:** Pass paths relative to the project root — \\\`src/app/page.tsx\\\`, NOT \\\`/workspace/\${activeProject.slug ?? "<slug>"}/src/app/page.tsx\\\` and NOT \\\`\${activeProject.slug ?? "<slug>"}/src/app/page.tsx\\\`. The tool layer rejects writes outside the project root with a \\\`PATH_OUTSIDE_PROJECT\\\` error suggesting the corrected path.`;
|
|
||||||
code = code.replace(oldPathConv, newPathConv);
|
|
||||||
|
|
||||||
// Fix 8: fs_tree recommendation
|
|
||||||
const devContainerStart = `**Start a coding session:** \\\`devcontainer_ensure { projectId }\\\` (idempotent; first call ~10s, then instant).`;
|
|
||||||
const devContainerStartNew = `**Start a coding session:** \\\`devcontainer_ensure { projectId }\\\` (idempotent; first call ~10s, then instant).
|
|
||||||
|
|
||||||
**Orient yourself once.** On the first code-modifying turn of a chat, call \\\`fs_tree\\\` once to learn the repo layout. Don't re-run it on every turn — the layout doesn't change between user messages.`;
|
|
||||||
code = code.replace(devContainerStart, devContainerStartNew);
|
|
||||||
|
|
||||||
// Fix 9: browser_console
|
|
||||||
const visualQaBlock = `**Visual QA:** \\\`request_visual_qa { targetPath }\\\` critiques a UI file against a 5-dim design rubric. **Call this whenever you modify visual UI code** before returning the \\\`previewUrl\\\`. If it returns actionable issues, fix them with \\\`fs_edit\\\` before ending the turn. Skip for backend / SQL / config / non-visual changes.`;
|
|
||||||
const visualQaBlockNew = `**Visual QA:** \\\`request_visual_qa { targetPath }\\\` critiques a UI file against a 5-dim design rubric. **Call this whenever you modify visual UI code** before returning the \\\`previewUrl\\\`. If it returns actionable issues, fix them with \\\`fs_edit\\\` before ending the turn. Skip for backend / SQL / config / non-visual changes.
|
|
||||||
|
|
||||||
**Verify the page actually renders:**
|
|
||||||
- After \\\`dev_server_start\\\` returns a \\\`previewUrl\\\` AND \\\`healthCheck.status === 200\\\`, for any UI-facing turn, call \\\`browser_console { url: previewUrl }\\\` to capture frontend console errors. Hydration errors, missing assets, and uncaught exceptions show up here even when the server is technically "running".
|
|
||||||
- If \\\`browser_console\\\` returns errors, fix them with \\\`fs_edit\\\` before declaring done. A green \\\`healthCheck\\\` plus a clean console is the real "done" signal for UI work.
|
|
||||||
- Skip this for backend / SQL / config-only changes.`;
|
|
||||||
code = code.replace(visualQaBlock, visualQaBlockNew);
|
|
||||||
|
|
||||||
// Fix 10: Market research stack
|
|
||||||
const commonQuestionsBlock = `## Common questions → tools
|
|
||||||
- "What is project X?" → \\\`projects_get { projectId }\\\`
|
|
||||||
- "What's running / has a domain?" → \\\`apps_list { projectId }\\\` (or workspace-wide without projectId)
|
|
||||||
- "Show logs / containers / env" → \\\`apps_list\\\` to resolve uuid, then \\\`apps_logs\\\` / \\\`apps_containers_list\\\` / \\\`apps_envs_list\\\`
|
|
||||||
- "Find an OSS X" → \\\`github_search\\\` (include \\\`license:mit\\\` by default), then \\\`github_file\\\` to read README / docker-compose
|
|
||||||
- "What do the docs say about Y?" → \\\`http_fetch\\\``;
|
|
||||||
|
|
||||||
// Oops, we changed apps_containers_list to apps_containers_ps in Fix 4, so let's match the updated one:
|
|
||||||
const commonQuestionsBlockMatched = `## Common questions → tools
|
|
||||||
- "What is project X?" → \\\`projects_get { projectId }\\\`
|
|
||||||
- "What's running / has a domain?" → \\\`apps_list { projectId }\\\` (or workspace-wide without projectId)
|
|
||||||
- "Show logs / containers / env" → \\\`apps_list\\\` to resolve uuid, then \\\`apps_logs\\\` / \\\`apps_containers_ps\\\` / \\\`apps_envs_list\\\`
|
|
||||||
- "Find an OSS X" → \\\`github_search\\\` (include \\\`license:mit\\\` by default), then \\\`github_file\\\` to read README / docker-compose
|
|
||||||
- "What do the docs say about Y?" → \\\`http_fetch\\\``;
|
|
||||||
|
|
||||||
const marketResearchBlock = `
|
|
||||||
|
|
||||||
## Helping the user pick what to build
|
|
||||||
|
|
||||||
Vibn has a market-research toolkit for non-technical founders who need data on their target niche. Use it when the user is undecided, validating an idea, or comparing markets:
|
|
||||||
|
|
||||||
- **"How big is the market for X in <location>?"** → \\\`market_categories_suggest { niche }\\\` to propose Google Business categories, then \\\`market_research_run\\\` after the user approves. Returns TAM count, sample domains, and review data. NOTE: \\\`market_research_run\\\` costs real money — always confirm with the user and pass \\\`user_explicitly_approved: true\\\`.
|
|
||||||
- **"What are competitors spending on Google Ads?"** → \\\`market_seo_analyze { domain }\\\`. Returns organic traffic, paid traffic, ad spend, and top paid keywords. Use to tell the user how aggressive a market is.
|
|
||||||
- **"What software do these businesses already use?"** → \\\`tech_stack_analyze { urls, software_category_id }\\\`. Detects WordPress, Shopify, named competitors, and any custom domains/scripts you pass. Use to find "X businesses use WordPress but lack Y" market gaps.
|
|
||||||
- **"What are customers complaining about?"** → \\\`market_aggregate_insights { category, location }\\\`. Returns top review topics — use the actual words customers use as marketing copy and value-prop seeds.
|
|
||||||
- **"Who are the players in this niche?"** → \\\`market_competitor_research { niche }\\\`. Returns proprietary competitors with pricing AND open-source alternatives that could be forked.
|
|
||||||
|
|
||||||
These are conversational research tools — they don't build anything. Use them BEFORE scaffolding when the user is exploring direction; SKIP them once the user has committed to building.`;
|
|
||||||
|
|
||||||
code = code.replace(commonQuestionsBlockMatched, commonQuestionsBlockMatched + marketResearchBlock);
|
|
||||||
|
|
||||||
fs.writeFileSync(file, code);
|
|
||||||
console.log("Applied Phase 2 and 3 prompt fixes");
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const file = 'vibn-agent-runner/src/tools/context.ts';
|
|
||||||
let code = fs.readFileSync(file, 'utf8');
|
|
||||||
|
|
||||||
const newProps = ` coolify: {
|
|
||||||
apiUrl: string;
|
|
||||||
apiToken: string;
|
|
||||||
};
|
|
||||||
mcpToken: string;
|
|
||||||
vibnApiUrl: string;
|
|
||||||
projectId?: string;`;
|
|
||||||
|
|
||||||
code = code.replace(` coolify: {
|
|
||||||
apiUrl: string;
|
|
||||||
apiToken: string;
|
|
||||||
};`, newProps);
|
|
||||||
|
|
||||||
fs.writeFileSync(file, code);
|
|
||||||
console.log("Patched context.ts");
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const file = 'vibn-agent-runner/src/server.ts';
|
|
||||||
let code = fs.readFileSync(file, 'utf8');
|
|
||||||
|
|
||||||
// Update the type signature for the request body
|
|
||||||
const oldSig = ` } = req.body as {
|
|
||||||
sessionId?: string;
|
|
||||||
projectId?: string;
|
|
||||||
appName?: string;
|
|
||||||
appPath?: string;
|
|
||||||
giteaRepo?: string;
|
|
||||||
task?: string;
|
|
||||||
continueTask?: boolean;
|
|
||||||
autoApprove?: boolean;
|
|
||||||
coolifyAppUuid?: string;
|
|
||||||
};`;
|
|
||||||
|
|
||||||
const newSig = ` mcpToken, vibnApiUrl
|
|
||||||
} = req.body as {
|
|
||||||
sessionId?: string;
|
|
||||||
projectId?: string;
|
|
||||||
appName?: string;
|
|
||||||
appPath?: string;
|
|
||||||
giteaRepo?: string;
|
|
||||||
task?: string;
|
|
||||||
continueTask?: boolean;
|
|
||||||
autoApprove?: boolean;
|
|
||||||
coolifyAppUuid?: string;
|
|
||||||
mcpToken?: string;
|
|
||||||
vibnApiUrl?: string;
|
|
||||||
};`;
|
|
||||||
|
|
||||||
code = code.replace(oldSig, newSig);
|
|
||||||
|
|
||||||
const oldCtx = ` const ctx: ToolContext = {
|
|
||||||
workspaceRoot: repoRoot,
|
|
||||||
gitea: {
|
|
||||||
apiUrl: GITEA_API_URL,
|
|
||||||
apiToken: GITEA_API_TOKEN,
|
|
||||||
username: GITEA_USERNAME,
|
|
||||||
},
|
|
||||||
coolify: {
|
|
||||||
apiUrl: process.env.COOLIFY_API_URL || '',
|
|
||||||
apiToken: process.env.COOLIFY_API_TOKEN || '',
|
|
||||||
},
|
|
||||||
memoryUpdates: [],
|
|
||||||
};`;
|
|
||||||
|
|
||||||
const newCtx = ` const ctx: ToolContext = {
|
|
||||||
workspaceRoot: repoRoot,
|
|
||||||
gitea: {
|
|
||||||
apiUrl: GITEA_API_URL,
|
|
||||||
apiToken: GITEA_API_TOKEN,
|
|
||||||
username: GITEA_USERNAME,
|
|
||||||
},
|
|
||||||
coolify: {
|
|
||||||
apiUrl: process.env.COOLIFY_API_URL || '',
|
|
||||||
apiToken: process.env.COOLIFY_API_TOKEN || '',
|
|
||||||
},
|
|
||||||
mcpToken: mcpToken || '',
|
|
||||||
vibnApiUrl: vibnApiUrl || 'http://localhost:3000',
|
|
||||||
projectId,
|
|
||||||
memoryUpdates: [],
|
|
||||||
};`;
|
|
||||||
|
|
||||||
code = code.replace(oldCtx, newCtx);
|
|
||||||
|
|
||||||
fs.writeFileSync(file, code);
|
|
||||||
console.log("Patched Runner server.ts");
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
const file = 'vibn-frontend/app/api/projects/[projectId]/agent/sessions/route.ts';
|
|
||||||
let code = fs.readFileSync(file, 'utf8');
|
|
||||||
|
|
||||||
// Inject the workspace API key fetching logic
|
|
||||||
if (!code.includes('listWorkspaceApiKeys')) {
|
|
||||||
code = code.replace(
|
|
||||||
'import { query } from "@/lib/db-postgres";',
|
|
||||||
'import { query } from "@/lib/db-postgres";\nimport { listWorkspaceApiKeys, mintWorkspaceApiKey, revealWorkspaceApiKey } from "@/lib/auth/workspace-auth";'
|
|
||||||
);
|
|
||||||
|
|
||||||
const injectCode = `
|
|
||||||
const wsResult = await query<{ workspace_id: string }>(
|
|
||||||
\`SELECT vibn_workspace_id as workspace_id FROM fs_projects WHERE id = $1 LIMIT 1\`,
|
|
||||||
[projectId]
|
|
||||||
);
|
|
||||||
if (!wsResult.length) {
|
|
||||||
return NextResponse.json({ error: "Project not found" }, { status: 404 });
|
|
||||||
}
|
|
||||||
const workspaceId = wsResult[0].workspace_id;
|
|
||||||
|
|
||||||
// Grab or mint a default API key for the runner to use
|
|
||||||
let mcpToken = "";
|
|
||||||
const keys = await listWorkspaceApiKeys(workspaceId);
|
|
||||||
let defaultKey = keys.find((k: any) => k.name === 'default' && !k.revoked_at);
|
|
||||||
if (!defaultKey) {
|
|
||||||
const minted = await mintWorkspaceApiKey({ workspaceId, name: 'default', createdBy: session.user.id, scopes: ['workspace:*'] });
|
|
||||||
mcpToken = minted.token;
|
|
||||||
} else {
|
|
||||||
const revealed = await revealWorkspaceApiKey(workspaceId, defaultKey.id);
|
|
||||||
if (revealed) mcpToken = revealed.token;
|
|
||||||
else {
|
|
||||||
const minted = await mintWorkspaceApiKey({ workspaceId, name: 'default', createdBy: session.user.id, scopes: ['workspace:*'] });
|
|
||||||
mcpToken = minted.token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add VIBN_API_URL so the runner knows where to send MCP requests
|
|
||||||
const vibnApiUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
|
||||||
`;
|
|
||||||
|
|
||||||
code = code.replace(
|
|
||||||
'const sessionId = rows[0].id;',
|
|
||||||
'const sessionId = rows[0].id;\n' + injectCode
|
|
||||||
);
|
|
||||||
|
|
||||||
code = code.replace(
|
|
||||||
'coolifyAppUuid,\n }),',
|
|
||||||
'coolifyAppUuid,\n mcpToken,\n vibnApiUrl\n }),'
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.writeFileSync(file, code);
|
|
||||||
console.log("Patched session route to forward MCP token");
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user