mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 21:05:51 +00:00
Compare commits
3 Commits
codex/code
...
codex/memo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
379928fa10 | ||
|
|
ee0d788e58 | ||
|
|
f353eb056a |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-code-best",
|
||||
"version": "1.10.4",
|
||||
"version": "1.10.2",
|
||||
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
||||
"type": "module",
|
||||
"author": "claude-code-best <claude-code-best@proton.me>",
|
||||
|
||||
@@ -616,7 +616,10 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
||||
case 'shutdown_response':
|
||||
return `shutdown_response ${input.message.approve ? 'approve' : 'reject'} ${input.message.request_id}`
|
||||
case 'plan_approval_response':
|
||||
return `plan_approval ${input.message.approve ? 'approve' : 'reject'} to ${recipient}`
|
||||
const planApprovalDecision = input.message.approve
|
||||
? 'approve'
|
||||
: 'reject'
|
||||
return `plan_approval ${planApprovalDecision} to ${recipient}`
|
||||
}
|
||||
},
|
||||
|
||||
@@ -834,10 +837,10 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
||||
const { postInterClaudeMessage } =
|
||||
require('src/bridge/peerSessions.js') as typeof import('src/bridge/peerSessions.js')
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
const result = (await postInterClaudeMessage(
|
||||
const result = await postInterClaudeMessage(
|
||||
addr.target,
|
||||
input.message,
|
||||
)) as { ok: boolean; error?: string }
|
||||
) as { ok: boolean; error?: string }
|
||||
const preview = input.summary || truncate(input.message, 50)
|
||||
return {
|
||||
data: {
|
||||
@@ -849,7 +852,6 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
||||
}
|
||||
}
|
||||
if (addr.scheme === 'uds') {
|
||||
const recipient = recipientForDisplay(input.to)
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const { sendToUdsSocket } =
|
||||
require('src/utils/udsClient.js') as typeof import('src/utils/udsClient.js')
|
||||
@@ -860,14 +862,14 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
||||
return {
|
||||
data: {
|
||||
success: true,
|
||||
message: `”${preview}” → ${recipient}`,
|
||||
message: `”${preview}” → ${input.to}`,
|
||||
},
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
data: {
|
||||
success: false,
|
||||
message: `Failed to send to ${recipient}: ${errorMessage(e)}`,
|
||||
message: `Failed to send to ${input.to}: ${errorMessage(e)}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,6 @@ import {
|
||||
getOriginalCwd,
|
||||
getSessionId,
|
||||
regenerateSessionId,
|
||||
resetCostState,
|
||||
setLastAPIRequest,
|
||||
setLastAPIRequestMessages,
|
||||
setLastClassifierRequests,
|
||||
} from '../../bootstrap/state.js'
|
||||
import type { SDKStatusMessage } from '../../entrypoints/sdk/coreTypes.js'
|
||||
import {
|
||||
@@ -148,14 +144,6 @@ export async function clearConversation({
|
||||
// tracking) is retained so those agents keep functioning.
|
||||
clearSessionCaches(preservedAgentIds)
|
||||
|
||||
// Clear large STATE-held data that outlives the message array.
|
||||
// lastAPIRequestMessages can hold the full post-compaction conversation
|
||||
// (hundreds of KB–MB) for /share; resetCostState clears modelUsage.
|
||||
setLastAPIRequest(null)
|
||||
setLastAPIRequestMessages(null)
|
||||
setLastClassifierRequests(null)
|
||||
resetCostState()
|
||||
|
||||
setCwd(getOriginalCwd())
|
||||
readFileState.clear()
|
||||
discoveredSkillNames?.clear()
|
||||
|
||||
@@ -3051,22 +3051,12 @@ export function REPL({
|
||||
// are O(n) per render, so drop everything before the previous
|
||||
// boundary to keep n bounded across multi-day sessions.
|
||||
if (isFullscreenEnvEnabled()) {
|
||||
setMessages(old => {
|
||||
const postBoundary = getMessagesAfterCompactBoundary(old, {
|
||||
setMessages(old => [
|
||||
...getMessagesAfterCompactBoundary(old, {
|
||||
includeSnipped: true,
|
||||
})
|
||||
// Hard cap: keep at most 500 messages in fullscreen scrollback
|
||||
// to prevent unbounded memory growth in multi-day sessions.
|
||||
// normalizeMessages/applyGrouping are O(n), and Ink fiber
|
||||
// trees cost ~250KB RSS per message. Without this cap,
|
||||
// scrollback after several compactions can reach thousands
|
||||
// of messages (observed: 13k+, 1GB+ heap).
|
||||
const MAX_FULLSCREEN_SCROLLBACK = 500
|
||||
const kept = postBoundary.length > MAX_FULLSCREEN_SCROLLBACK
|
||||
? postBoundary.slice(-MAX_FULLSCREEN_SCROLLBACK)
|
||||
: postBoundary
|
||||
return [...kept, newMessage]
|
||||
});
|
||||
}),
|
||||
newMessage,
|
||||
]);
|
||||
} else {
|
||||
setMessages(() => [newMessage]);
|
||||
}
|
||||
@@ -3092,23 +3082,17 @@ export function REPL({
|
||||
// history). Replacing those leaves the AgentTool UI stuck at
|
||||
// "Initializing…" because it renders the full progress trail.
|
||||
setMessages(oldMessages => {
|
||||
const last = oldMessages.at(-1);
|
||||
const lastData = last?.data as Record<string, unknown> | undefined;
|
||||
const newData = newMessage.data as Record<string, unknown>;
|
||||
// Scan backwards to find the last ephemeral progress with matching
|
||||
// parentToolUseID and type. Previously only checked the last message,
|
||||
// so interleaved non-ephemeral messages caused duplicate progress
|
||||
// entries to accumulate (observed 13k+ entries in sleep-heavy sessions).
|
||||
for (let i = oldMessages.length - 1; i >= 0; i--) {
|
||||
const m = oldMessages[i]!
|
||||
if (m.type !== 'progress') break
|
||||
const mData = m.data as Record<string, unknown> | undefined
|
||||
if (
|
||||
m.parentToolUseID === newMessage.parentToolUseID &&
|
||||
mData?.type === newData.type
|
||||
) {
|
||||
const copy = oldMessages.slice();
|
||||
copy[i] = newMessage;
|
||||
return copy;
|
||||
}
|
||||
if (
|
||||
last?.type === 'progress' &&
|
||||
last.parentToolUseID === newMessage.parentToolUseID &&
|
||||
lastData?.type === newData.type
|
||||
) {
|
||||
const copy = oldMessages.slice();
|
||||
copy[copy.length - 1] = newMessage;
|
||||
return copy;
|
||||
}
|
||||
return [...oldMessages, newMessage];
|
||||
});
|
||||
|
||||
@@ -33,8 +33,6 @@ describe('startAgentSummarization', () => {
|
||||
let debugLogs: string[]
|
||||
let loggedErrors: Error[]
|
||||
let clearedHandles: unknown[]
|
||||
let scheduledCount: number
|
||||
let lastTimerHandle: unknown
|
||||
|
||||
function startTestSummarization(
|
||||
dependencies: AgentSummaryDependencies = {},
|
||||
@@ -83,10 +81,8 @@ describe('startAgentSummarization', () => {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new Error('Expected timer callback')
|
||||
}
|
||||
scheduledCount += 1
|
||||
scheduled = callback as () => void | Promise<void>
|
||||
lastTimerHandle = { id: scheduledCount }
|
||||
return lastTimerHandle as ReturnType<typeof setTimeout>
|
||||
return 1 as unknown as ReturnType<typeof setTimeout>
|
||||
}) as unknown as typeof setTimeout,
|
||||
updateAgentSummary: (taskId: string, summary: string) => {
|
||||
updateCalls.push({ taskId, summary })
|
||||
@@ -105,14 +101,8 @@ describe('startAgentSummarization', () => {
|
||||
debugLogs = []
|
||||
loggedErrors = []
|
||||
clearedHandles = []
|
||||
scheduledCount = 0
|
||||
lastTimerHandle = undefined
|
||||
})
|
||||
|
||||
function expectDebugLogContaining(fragment: string): void {
|
||||
expect(debugLogs.some(message => message.includes(fragment))).toBe(true)
|
||||
}
|
||||
|
||||
test('summarizes bounded transcript once and skips unchanged fingerprints', async () => {
|
||||
handle = startTestSummarization()
|
||||
|
||||
@@ -138,7 +128,6 @@ describe('startAgentSummarization', () => {
|
||||
|
||||
expect(forkCalls).toHaveLength(1)
|
||||
expect(updateCalls).toHaveLength(1)
|
||||
expect(loggedErrors).toEqual([])
|
||||
})
|
||||
|
||||
test('skips summarization when filtering leaves too little bounded context', async () => {
|
||||
@@ -161,7 +150,9 @@ describe('startAgentSummarization', () => {
|
||||
|
||||
expect(forkCalls).toEqual([])
|
||||
expect(updateCalls).toEqual([])
|
||||
expectDebugLogContaining('no bounded context available')
|
||||
expect(debugLogs).toContain(
|
||||
'[AgentSummary] Skipping summary for task-1: no bounded context available',
|
||||
)
|
||||
})
|
||||
|
||||
test('skips summarization before building context when transcript is too short', async () => {
|
||||
@@ -173,7 +164,9 @@ describe('startAgentSummarization', () => {
|
||||
|
||||
expect(forkCalls).toEqual([])
|
||||
expect(updateCalls).toEqual([])
|
||||
expectDebugLogContaining('not enough messages (2)')
|
||||
expect(debugLogs).toContain(
|
||||
'[AgentSummary] Skipping summary for task-1: not enough messages (2)',
|
||||
)
|
||||
})
|
||||
|
||||
test('skips and reschedules while poor mode is active', async () => {
|
||||
@@ -182,18 +175,16 @@ describe('startAgentSummarization', () => {
|
||||
})
|
||||
|
||||
expect(typeof scheduled).toBe('function')
|
||||
const initialScheduledCount = scheduledCount
|
||||
const initialTimerHandle = lastTimerHandle
|
||||
await scheduled!()
|
||||
|
||||
expect(forkCalls).toEqual([])
|
||||
expect(updateCalls).toEqual([])
|
||||
expectDebugLogContaining('poor mode active')
|
||||
expect(scheduledCount).toBe(initialScheduledCount + 1)
|
||||
expect(lastTimerHandle).not.toBe(initialTimerHandle)
|
||||
expect(debugLogs).toContain(
|
||||
'[AgentSummary] Skipping summary — poor mode active',
|
||||
)
|
||||
})
|
||||
|
||||
test('logs summary errors and schedules the next timer', async () => {
|
||||
test('logs summary errors and keeps the next timer owned by the summarizer', async () => {
|
||||
const error = new Error('fork failed')
|
||||
handle = startTestSummarization({
|
||||
runForkedAgent: async () => {
|
||||
@@ -202,23 +193,20 @@ describe('startAgentSummarization', () => {
|
||||
})
|
||||
|
||||
expect(typeof scheduled).toBe('function')
|
||||
const initialScheduledCount = scheduledCount
|
||||
const initialTimerHandle = lastTimerHandle
|
||||
await scheduled!()
|
||||
|
||||
expect(loggedErrors).toEqual([error])
|
||||
expect(updateCalls).toEqual([])
|
||||
expect(scheduledCount).toBe(initialScheduledCount + 1)
|
||||
expect(lastTimerHandle).not.toBe(initialTimerHandle)
|
||||
})
|
||||
|
||||
test('stop clears the pending summary timer', () => {
|
||||
handle = startTestSummarization()
|
||||
const pendingHandle = lastTimerHandle
|
||||
|
||||
handle.stop()
|
||||
|
||||
expectDebugLogContaining('Stopping summarization for task-1')
|
||||
expect(clearedHandles).toEqual([pendingHandle])
|
||||
expect(debugLogs).toContain(
|
||||
'[AgentSummary] Stopping summarization for task-1',
|
||||
)
|
||||
expect(clearedHandles).toEqual([1])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { mkdtempSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import type { Message } from 'src/types/message.js'
|
||||
import { getErrnoCode } from 'src/utils/errors.js'
|
||||
import {
|
||||
compactMailboxMessages,
|
||||
getLastPeerDmSummary,
|
||||
@@ -347,30 +346,17 @@ describe('teammate mailbox retention', () => {
|
||||
const inboxPath = getInboxPath('worker', 'alpha')
|
||||
await mkdir(inboxPath, { recursive: true })
|
||||
|
||||
const error = await writeToMailbox(
|
||||
'worker',
|
||||
{
|
||||
from: 'team-lead',
|
||||
text: 'new',
|
||||
timestamp: new Date(5).toISOString(),
|
||||
},
|
||||
'alpha',
|
||||
).then(
|
||||
() => undefined,
|
||||
err => err,
|
||||
)
|
||||
|
||||
const code = getErrnoCode(error)
|
||||
expect(code).toBeDefined()
|
||||
if (code === undefined) {
|
||||
throw new Error('Expected filesystem errno code')
|
||||
}
|
||||
const expectedCodes =
|
||||
process.platform === 'win32'
|
||||
? ['EISDIR', 'EPERM', 'EACCES']
|
||||
: ['EISDIR']
|
||||
expect(expectedCodes).toContain(code)
|
||||
expect((await stat(inboxPath)).isDirectory()).toBe(true)
|
||||
await expect(
|
||||
writeToMailbox(
|
||||
'worker',
|
||||
{
|
||||
from: 'team-lead',
|
||||
text: 'new',
|
||||
timestamp: new Date(5).toISOString(),
|
||||
},
|
||||
'alpha',
|
||||
),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
test('readMailbox fails closed on corrupt mailbox content', async () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
writeFile,
|
||||
} from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createConnection, createServer, type Socket } from 'node:net'
|
||||
import { createConnection, createServer } from 'node:net'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import {
|
||||
@@ -227,134 +227,11 @@ describe('UDS inbox retention', () => {
|
||||
JSON.stringify({ socketPath: path, authToken: 'test-token' }),
|
||||
'utf-8',
|
||||
)
|
||||
const { sendToUdsSocket, UdsPeerConnectionError } = await import(
|
||||
'../udsClient.js'
|
||||
const { sendToUdsSocket } = await import('../udsClient.js')
|
||||
|
||||
await expect(sendToUdsSocket(path, 'hello')).rejects.toThrow(
|
||||
'Failed to connect to peer',
|
||||
)
|
||||
|
||||
const error = await sendToUdsSocket(path, 'hello').then(
|
||||
() => undefined,
|
||||
err => err,
|
||||
)
|
||||
expect(error).toBeInstanceOf(UdsPeerConnectionError)
|
||||
if (!(error instanceof UdsPeerConnectionError)) {
|
||||
throw new Error('Expected UDS peer connection error')
|
||||
}
|
||||
expect(error.socketPath).toBe(path)
|
||||
expect(error.message).not.toContain('test-token')
|
||||
})
|
||||
|
||||
test('udsClient send reports response timeouts as peer connection errors', async () => {
|
||||
const path = socketPath('uds-client-timeout')
|
||||
const capabilityDir = join(tempConfigDir, 'messaging-capabilities')
|
||||
const capabilityName = `${createHash('sha256').update(path).digest('hex')}.json`
|
||||
await mkdir(capabilityDir, { recursive: true, mode: 0o700 })
|
||||
await writeFile(
|
||||
join(capabilityDir, capabilityName),
|
||||
JSON.stringify({ socketPath: path, authToken: 'test-token' }),
|
||||
'utf-8',
|
||||
)
|
||||
if (process.platform !== 'win32') {
|
||||
await mkdir(dirname(path), { recursive: true })
|
||||
}
|
||||
|
||||
const sockets = new Set<Socket>()
|
||||
const receiver = createServer(socket => {
|
||||
sockets.add(socket)
|
||||
socket.on('close', () => {
|
||||
sockets.delete(socket)
|
||||
})
|
||||
socket.on('data', () => undefined)
|
||||
})
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
receiver.on('error', reject)
|
||||
receiver.listen(path, () => resolve())
|
||||
})
|
||||
|
||||
try {
|
||||
const { sendToUdsSocket, UdsPeerConnectionError } = await import(
|
||||
'../udsClient.js'
|
||||
)
|
||||
|
||||
const error = await sendToUdsSocket(path, 'hello', 200).then(
|
||||
() => undefined,
|
||||
err => err,
|
||||
)
|
||||
expect(error).toBeInstanceOf(UdsPeerConnectionError)
|
||||
if (!(error instanceof UdsPeerConnectionError)) {
|
||||
throw new Error('Expected UDS peer connection timeout error')
|
||||
}
|
||||
expect(error.socketPath).toBe(path)
|
||||
expect(error.cause).toBeInstanceOf(Error)
|
||||
if (!(error.cause instanceof Error)) {
|
||||
throw new Error('Expected timeout cause')
|
||||
}
|
||||
expect(error.cause.message).toBe('Connection timed out')
|
||||
expect(error.message).not.toContain('test-token')
|
||||
} finally {
|
||||
for (const socket of sockets) {
|
||||
socket.destroy()
|
||||
}
|
||||
await closeServer(receiver)
|
||||
if (process.platform !== 'win32') {
|
||||
await unlink(path).catch(() => undefined)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('connectToPeer reports connection failures as peer connection errors', async () => {
|
||||
const path = socketPath('uds-connect-error')
|
||||
const { connectToPeer, UdsPeerConnectionError } = await import(
|
||||
'../udsClient.js'
|
||||
)
|
||||
|
||||
const error = await connectToPeer(path).then(
|
||||
() => undefined,
|
||||
err => err,
|
||||
)
|
||||
|
||||
expect(error).toBeInstanceOf(UdsPeerConnectionError)
|
||||
if (!(error instanceof UdsPeerConnectionError)) {
|
||||
throw new Error('Expected UDS peer connection error')
|
||||
}
|
||||
expect(error.socketPath).toBe(path)
|
||||
})
|
||||
|
||||
test('connectToPeer leaves connected socket lifecycle to the caller', async () => {
|
||||
const path = socketPath('uds-connect-lifecycle')
|
||||
if (process.platform !== 'win32') {
|
||||
await mkdir(dirname(path), { recursive: true })
|
||||
}
|
||||
|
||||
const sockets = new Set<Socket>()
|
||||
const receiver = createServer(socket => {
|
||||
sockets.add(socket)
|
||||
socket.on('close', () => {
|
||||
sockets.delete(socket)
|
||||
})
|
||||
})
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
receiver.on('error', reject)
|
||||
receiver.listen(path, () => resolve())
|
||||
})
|
||||
|
||||
let client: Socket | undefined
|
||||
try {
|
||||
const { connectToPeer } = await import('../udsClient.js')
|
||||
client = await connectToPeer(path, 50)
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
expect(client.destroyed).toBe(false)
|
||||
expect(client.listenerCount('error')).toBe(0)
|
||||
} finally {
|
||||
client?.destroy()
|
||||
for (const socket of sockets) {
|
||||
socket.destroy()
|
||||
}
|
||||
await closeServer(receiver)
|
||||
if (process.platform !== 'win32') {
|
||||
await unlink(path).catch(() => undefined)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('sendUdsMessage fails closed before connecting without an auth token', async () => {
|
||||
|
||||
@@ -36,19 +36,6 @@ export type PeerSession = {
|
||||
alive: boolean
|
||||
}
|
||||
|
||||
export class UdsPeerConnectionError extends Error {
|
||||
readonly socketPath: string
|
||||
|
||||
constructor(socketPath: string, cause: unknown) {
|
||||
super(
|
||||
`Failed to connect to peer at ${socketPath}: ${errorMessage(cause)}`,
|
||||
{ cause },
|
||||
)
|
||||
this.name = 'UdsPeerConnectionError'
|
||||
this.socketPath = socketPath
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Session directory
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -206,7 +193,6 @@ export async function isPeerAlive(
|
||||
export async function sendToUdsSocket(
|
||||
targetSocketPath: string,
|
||||
message: string | Record<string, unknown>,
|
||||
timeoutMs = 5000,
|
||||
): Promise<void> {
|
||||
const { parseUdsTarget } = await import('./udsMessaging.js')
|
||||
const target = parseUdsTarget(targetSocketPath)
|
||||
@@ -251,15 +237,12 @@ export async function sendToUdsSocket(
|
||||
maxFrameBytes: MAX_UDS_FRAME_BYTES,
|
||||
onSettled: finish,
|
||||
formatSocketError: err =>
|
||||
new UdsPeerConnectionError(target.socketPath, err),
|
||||
})
|
||||
conn.setTimeout(timeoutMs, () => {
|
||||
finish(
|
||||
new UdsPeerConnectionError(
|
||||
target.socketPath,
|
||||
new Error('Connection timed out'),
|
||||
new Error(
|
||||
`Failed to connect to peer at ${target.socketPath}: ${errorMessage(err)}`,
|
||||
),
|
||||
)
|
||||
})
|
||||
conn.setTimeout(5000, () => {
|
||||
finish(new Error('Connection timed out'))
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -268,30 +251,14 @@ export async function sendToUdsSocket(
|
||||
* Connect to a peer and return the raw socket for bidirectional communication.
|
||||
* The caller is responsible for managing the connection lifecycle.
|
||||
*/
|
||||
export function connectToPeer(
|
||||
socketPath: string,
|
||||
timeoutMs = 5000,
|
||||
): Promise<Socket> {
|
||||
export function connectToPeer(socketPath: string): Promise<Socket> {
|
||||
return new Promise<Socket>((resolve, reject) => {
|
||||
const conn = createConnection(socketPath)
|
||||
let settled = false
|
||||
const fail = (cause: unknown) => {
|
||||
if (settled) {
|
||||
return
|
||||
}
|
||||
settled = true
|
||||
conn.destroy()
|
||||
reject(new UdsPeerConnectionError(socketPath, cause))
|
||||
}
|
||||
conn.once('connect', () => {
|
||||
settled = true
|
||||
conn.setTimeout(0)
|
||||
conn.off('error', fail)
|
||||
const conn = createConnection(socketPath, () => {
|
||||
resolve(conn)
|
||||
})
|
||||
conn.on('error', fail)
|
||||
conn.setTimeout(timeoutMs, () => {
|
||||
fail(new Error('Connection timed out'))
|
||||
conn.on('error', reject)
|
||||
conn.setTimeout(5000, () => {
|
||||
conn.destroy(new Error('Connection timed out'))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user