@apa-network/agent-sdk 0.1.0 → 0.2.0-beta.10
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.
- package/README.md +109 -14
- package/bin/apa-bot.js +6 -4
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +491 -74
- package/dist/cli.next_decision.e2e.test.js +468 -0
- package/dist/commands/register.d.ts +2 -0
- package/dist/commands/register.js +18 -0
- package/dist/commands/register.test.js +26 -0
- package/dist/commands/session_recovery.d.ts +6 -0
- package/dist/commands/session_recovery.js +25 -0
- package/dist/commands/session_recovery.test.js +27 -0
- package/dist/http/client.d.ts +30 -1
- package/dist/http/client.js +50 -7
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -1
- package/dist/loop/callback.d.ts +20 -0
- package/dist/loop/callback.js +105 -0
- package/dist/loop/callback.test.js +33 -0
- package/dist/loop/credentials.d.ts +10 -0
- package/dist/loop/credentials.js +60 -0
- package/dist/loop/credentials.test.d.ts +1 -0
- package/dist/loop/credentials.test.js +33 -0
- package/dist/loop/decision_state.d.ts +30 -0
- package/dist/loop/decision_state.js +43 -0
- package/dist/loop/state.d.ts +9 -0
- package/dist/loop/state.js +16 -0
- package/dist/loop/state.test.d.ts +1 -0
- package/dist/loop/state.test.js +14 -0
- package/dist/utils/config.d.ts +1 -2
- package/dist/utils/config.js +8 -5
- package/dist/utils/config.test.js +7 -1
- package/package.json +7 -2
- package/dist/bot/createBot.d.ts +0 -14
- package/dist/bot/createBot.js +0 -107
- package/dist/types/bot.d.ts +0 -42
- package/dist/types/messages.d.ts +0 -85
- package/dist/utils/action.d.ts +0 -3
- package/dist/utils/action.js +0 -10
- package/dist/utils/action.test.js +0 -10
- package/dist/utils/backoff.d.ts +0 -2
- package/dist/utils/backoff.js +0 -11
- package/dist/utils/backoff.test.js +0 -8
- package/dist/ws/client.d.ts +0 -32
- package/dist/ws/client.js +0 -116
- /package/dist/{types/bot.js → cli.next_decision.e2e.test.d.ts} +0 -0
- /package/dist/{types/messages.js → commands/register.test.d.ts} +0 -0
- /package/dist/{utils/action.test.d.ts → commands/session_recovery.test.d.ts} +0 -0
- /package/dist/{utils/backoff.test.d.ts → loop/callback.test.d.ts} +0 -0
package/dist/bot/createBot.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "node:events";
|
|
2
|
-
import { APAWsClient } from "../ws/client.js";
|
|
3
|
-
import { nextRequestId, validateBotAction } from "../utils/action.js";
|
|
4
|
-
const DEFAULT_GUARD_MS = 300;
|
|
5
|
-
function withTimeout(promise, timeoutMs) {
|
|
6
|
-
return new Promise((resolve, reject) => {
|
|
7
|
-
const timer = setTimeout(() => reject(new Error("strategy_timeout")), timeoutMs);
|
|
8
|
-
promise
|
|
9
|
-
.then((value) => {
|
|
10
|
-
clearTimeout(timer);
|
|
11
|
-
resolve(value);
|
|
12
|
-
})
|
|
13
|
-
.catch((err) => {
|
|
14
|
-
clearTimeout(timer);
|
|
15
|
-
reject(err);
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
function buildTurnKey(s) {
|
|
20
|
-
return `${s.hand_id}:${s.street}:${s.current_actor_seat}:${s.current_bet}:${s.call_amount}:${s.pot}`;
|
|
21
|
-
}
|
|
22
|
-
export function createBot(opts) {
|
|
23
|
-
const emitter = new EventEmitter();
|
|
24
|
-
const ws = new APAWsClient({
|
|
25
|
-
wsUrl: opts.wsUrl,
|
|
26
|
-
agentId: opts.agentId,
|
|
27
|
-
apiKey: opts.apiKey,
|
|
28
|
-
join: opts.join,
|
|
29
|
-
reconnect: opts.reconnect
|
|
30
|
-
});
|
|
31
|
-
let running = false;
|
|
32
|
-
let lastTurnKey = "";
|
|
33
|
-
ws.on("join_result", (evt) => {
|
|
34
|
-
if (!evt.ok) {
|
|
35
|
-
emitter.emit("error", new Error(`join failed: ${evt.error || "unknown"}`));
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
emitter.emit("join", evt);
|
|
39
|
-
});
|
|
40
|
-
ws.on("event_log", (evt) => emitter.emit("eventLog", evt));
|
|
41
|
-
ws.on("hand_end", (evt) => {
|
|
42
|
-
lastTurnKey = "";
|
|
43
|
-
emitter.emit("handEnd", evt);
|
|
44
|
-
});
|
|
45
|
-
ws.on("error", (err) => emitter.emit("error", err));
|
|
46
|
-
async function play(strategy) {
|
|
47
|
-
running = true;
|
|
48
|
-
await ws.connect();
|
|
49
|
-
ws.on("state_update", async (state) => {
|
|
50
|
-
if (!running) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
if (state.current_actor_seat !== state.my_seat) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
const turnKey = buildTurnKey(state);
|
|
57
|
-
if (turnKey === lastTurnKey) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
lastTurnKey = turnKey;
|
|
61
|
-
const ctx = {
|
|
62
|
-
gameId: state.game_id,
|
|
63
|
-
handId: state.hand_id,
|
|
64
|
-
mySeat: state.my_seat,
|
|
65
|
-
currentActorSeat: state.current_actor_seat,
|
|
66
|
-
minRaise: state.min_raise,
|
|
67
|
-
currentBet: state.current_bet,
|
|
68
|
-
callAmount: state.call_amount,
|
|
69
|
-
myBalance: state.my_balance,
|
|
70
|
-
communityCards: state.community_cards,
|
|
71
|
-
holeCards: state.hole_cards || [],
|
|
72
|
-
raw: state
|
|
73
|
-
};
|
|
74
|
-
const safetyMs = Math.max(100, state.action_timeout_ms - (opts.actionTimeoutGuardMs ?? DEFAULT_GUARD_MS));
|
|
75
|
-
try {
|
|
76
|
-
const action = await withTimeout(Promise.resolve(strategy(ctx)), safetyMs);
|
|
77
|
-
validateBotAction(action);
|
|
78
|
-
await ws.sendAction({
|
|
79
|
-
type: "action",
|
|
80
|
-
request_id: nextRequestId(),
|
|
81
|
-
action: action.action,
|
|
82
|
-
amount: "amount" in action ? action.amount : undefined
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
await ws.sendAction({
|
|
87
|
-
type: "action",
|
|
88
|
-
request_id: nextRequestId(),
|
|
89
|
-
action: "fold"
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
ws.on("action_result", (res) => {
|
|
94
|
-
if (!res.ok) {
|
|
95
|
-
emitter.emit("error", new Error(`action failed: ${res.error || "unknown"} (${res.request_id})`));
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
async function stop() {
|
|
100
|
-
running = false;
|
|
101
|
-
await ws.stop();
|
|
102
|
-
}
|
|
103
|
-
function on(event, cb) {
|
|
104
|
-
emitter.on(event, cb);
|
|
105
|
-
}
|
|
106
|
-
return { play, stop, on };
|
|
107
|
-
}
|
package/dist/types/bot.d.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { JoinMode, StateUpdateEvent } from "./messages.js";
|
|
2
|
-
export type BotAction = {
|
|
3
|
-
action: "fold";
|
|
4
|
-
} | {
|
|
5
|
-
action: "check";
|
|
6
|
-
} | {
|
|
7
|
-
action: "call";
|
|
8
|
-
} | {
|
|
9
|
-
action: "raise";
|
|
10
|
-
amount: number;
|
|
11
|
-
} | {
|
|
12
|
-
action: "bet";
|
|
13
|
-
amount: number;
|
|
14
|
-
};
|
|
15
|
-
export type PlayContext = {
|
|
16
|
-
gameId: string;
|
|
17
|
-
handId: string;
|
|
18
|
-
mySeat: number;
|
|
19
|
-
currentActorSeat: number;
|
|
20
|
-
minRaise: number;
|
|
21
|
-
currentBet: number;
|
|
22
|
-
callAmount: number;
|
|
23
|
-
myBalance: number;
|
|
24
|
-
communityCards: string[];
|
|
25
|
-
holeCards: string[];
|
|
26
|
-
raw: StateUpdateEvent;
|
|
27
|
-
};
|
|
28
|
-
export type StrategyFn = (ctx: PlayContext) => BotAction | Promise<BotAction>;
|
|
29
|
-
export type ReconnectOptions = {
|
|
30
|
-
enabled?: boolean;
|
|
31
|
-
baseMs?: number;
|
|
32
|
-
maxMs?: number;
|
|
33
|
-
jitter?: boolean;
|
|
34
|
-
};
|
|
35
|
-
export type CreateBotOptions = {
|
|
36
|
-
wsUrl?: string;
|
|
37
|
-
agentId: string;
|
|
38
|
-
apiKey: string;
|
|
39
|
-
join: JoinMode;
|
|
40
|
-
reconnect?: ReconnectOptions;
|
|
41
|
-
actionTimeoutGuardMs?: number;
|
|
42
|
-
};
|
package/dist/types/messages.d.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
export type JoinMode = {
|
|
2
|
-
mode: "random";
|
|
3
|
-
} | {
|
|
4
|
-
mode: "select";
|
|
5
|
-
roomId: string;
|
|
6
|
-
};
|
|
7
|
-
export type JoinMessage = {
|
|
8
|
-
type: "join";
|
|
9
|
-
agent_id: string;
|
|
10
|
-
api_key: string;
|
|
11
|
-
join_mode: "random" | "select";
|
|
12
|
-
room_id?: string;
|
|
13
|
-
};
|
|
14
|
-
export type ActionMessage = {
|
|
15
|
-
type: "action";
|
|
16
|
-
request_id: string;
|
|
17
|
-
action: "fold" | "check" | "call" | "raise" | "bet";
|
|
18
|
-
amount?: number;
|
|
19
|
-
thought_log?: string;
|
|
20
|
-
};
|
|
21
|
-
export type JoinResultEvent = {
|
|
22
|
-
type: "join_result";
|
|
23
|
-
protocol_version: string;
|
|
24
|
-
ok: boolean;
|
|
25
|
-
error?: string;
|
|
26
|
-
room_id?: string;
|
|
27
|
-
};
|
|
28
|
-
export type StateUpdateEvent = {
|
|
29
|
-
type: "state_update";
|
|
30
|
-
protocol_version: string;
|
|
31
|
-
game_id: string;
|
|
32
|
-
hand_id: string;
|
|
33
|
-
my_seat: number;
|
|
34
|
-
current_actor_seat: number;
|
|
35
|
-
min_raise: number;
|
|
36
|
-
current_bet: number;
|
|
37
|
-
call_amount: number;
|
|
38
|
-
my_balance: number;
|
|
39
|
-
action_timeout_ms: number;
|
|
40
|
-
street: string;
|
|
41
|
-
hole_cards?: string[];
|
|
42
|
-
community_cards: string[];
|
|
43
|
-
pot: number;
|
|
44
|
-
opponents: Array<{
|
|
45
|
-
seat: number;
|
|
46
|
-
name: string;
|
|
47
|
-
stack: number;
|
|
48
|
-
action: string;
|
|
49
|
-
}>;
|
|
50
|
-
};
|
|
51
|
-
export type ActionResultEvent = {
|
|
52
|
-
type: "action_result";
|
|
53
|
-
protocol_version: string;
|
|
54
|
-
request_id: string;
|
|
55
|
-
ok: boolean;
|
|
56
|
-
error?: string;
|
|
57
|
-
};
|
|
58
|
-
export type EventLogEvent = {
|
|
59
|
-
type: "event_log";
|
|
60
|
-
protocol_version: string;
|
|
61
|
-
timestamp_ms: number;
|
|
62
|
-
player_seat: number;
|
|
63
|
-
action: string;
|
|
64
|
-
amount?: number;
|
|
65
|
-
thought_log?: string;
|
|
66
|
-
event: string;
|
|
67
|
-
};
|
|
68
|
-
export type HandEndEvent = {
|
|
69
|
-
type: "hand_end";
|
|
70
|
-
protocol_version: string;
|
|
71
|
-
winner: string;
|
|
72
|
-
pot: number;
|
|
73
|
-
balances: Array<{
|
|
74
|
-
agent_id: string;
|
|
75
|
-
balance: number;
|
|
76
|
-
}>;
|
|
77
|
-
showdown?: Array<{
|
|
78
|
-
agent_id: string;
|
|
79
|
-
hole_cards: string[];
|
|
80
|
-
}>;
|
|
81
|
-
};
|
|
82
|
-
export type ServerEvent = JoinResultEvent | StateUpdateEvent | ActionResultEvent | EventLogEvent | HandEndEvent | {
|
|
83
|
-
type: string;
|
|
84
|
-
[k: string]: unknown;
|
|
85
|
-
};
|
package/dist/utils/action.d.ts
DELETED
package/dist/utils/action.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export function validateBotAction(action) {
|
|
2
|
-
if (action.action === "raise" || action.action === "bet") {
|
|
3
|
-
if (!Number.isFinite(action.amount) || action.amount <= 0) {
|
|
4
|
-
throw new Error(`${action.action} requires positive amount`);
|
|
5
|
-
}
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
export function nextRequestId() {
|
|
9
|
-
return `req_${Date.now()}_${Math.floor(Math.random() * 1_000_000_000)}`;
|
|
10
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { validateBotAction } from "./action.js";
|
|
4
|
-
test("validateBotAction accepts non-amount actions", () => {
|
|
5
|
-
validateBotAction({ action: "check" });
|
|
6
|
-
validateBotAction({ action: "fold" });
|
|
7
|
-
});
|
|
8
|
-
test("validateBotAction rejects invalid raise amount", () => {
|
|
9
|
-
assert.throws(() => validateBotAction({ action: "raise", amount: 0 }), /positive amount/);
|
|
10
|
-
});
|
package/dist/utils/backoff.d.ts
DELETED
package/dist/utils/backoff.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export function computeBackoffMs(attempt, baseMs, maxMs, jitter) {
|
|
2
|
-
const exponential = Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt - 1));
|
|
3
|
-
if (!jitter) {
|
|
4
|
-
return exponential;
|
|
5
|
-
}
|
|
6
|
-
const spread = Math.max(1, Math.floor(exponential * 0.2));
|
|
7
|
-
return exponential - spread + Math.floor(Math.random() * spread * 2);
|
|
8
|
-
}
|
|
9
|
-
export function sleep(ms) {
|
|
10
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { computeBackoffMs } from "./backoff.js";
|
|
4
|
-
test("computeBackoffMs grows exponentially without jitter", () => {
|
|
5
|
-
assert.equal(computeBackoffMs(1, 500, 8000, false), 500);
|
|
6
|
-
assert.equal(computeBackoffMs(2, 500, 8000, false), 1000);
|
|
7
|
-
assert.equal(computeBackoffMs(5, 500, 8000, false), 8000);
|
|
8
|
-
});
|
package/dist/ws/client.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "node:events";
|
|
2
|
-
import type { ActionMessage, JoinMode } from "../types/messages.js";
|
|
3
|
-
type WsOptions = {
|
|
4
|
-
wsUrl?: string;
|
|
5
|
-
agentId: string;
|
|
6
|
-
apiKey: string;
|
|
7
|
-
join: JoinMode;
|
|
8
|
-
reconnect?: {
|
|
9
|
-
enabled?: boolean;
|
|
10
|
-
baseMs?: number;
|
|
11
|
-
maxMs?: number;
|
|
12
|
-
jitter?: boolean;
|
|
13
|
-
};
|
|
14
|
-
};
|
|
15
|
-
export declare class APAWsClient extends EventEmitter {
|
|
16
|
-
private readonly wsUrl;
|
|
17
|
-
private readonly agentId;
|
|
18
|
-
private readonly apiKey;
|
|
19
|
-
private readonly join;
|
|
20
|
-
private readonly reconnect;
|
|
21
|
-
private socket;
|
|
22
|
-
private shouldRun;
|
|
23
|
-
private connectAttempt;
|
|
24
|
-
constructor(opts: WsOptions);
|
|
25
|
-
connect(): Promise<void>;
|
|
26
|
-
stop(): Promise<void>;
|
|
27
|
-
sendAction(action: ActionMessage): Promise<void>;
|
|
28
|
-
private openSocket;
|
|
29
|
-
private reconnectLoop;
|
|
30
|
-
private sendJoin;
|
|
31
|
-
}
|
|
32
|
-
export {};
|
package/dist/ws/client.js
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "node:events";
|
|
2
|
-
import { computeBackoffMs, sleep } from "../utils/backoff.js";
|
|
3
|
-
import { resolveWsUrl } from "../utils/config.js";
|
|
4
|
-
const DEFAULT_RECONNECT = {
|
|
5
|
-
enabled: true,
|
|
6
|
-
baseMs: 500,
|
|
7
|
-
maxMs: 8000,
|
|
8
|
-
jitter: true
|
|
9
|
-
};
|
|
10
|
-
export class APAWsClient extends EventEmitter {
|
|
11
|
-
wsUrl;
|
|
12
|
-
agentId;
|
|
13
|
-
apiKey;
|
|
14
|
-
join;
|
|
15
|
-
reconnect;
|
|
16
|
-
socket = null;
|
|
17
|
-
shouldRun = true;
|
|
18
|
-
connectAttempt = 0;
|
|
19
|
-
constructor(opts) {
|
|
20
|
-
super();
|
|
21
|
-
this.wsUrl = resolveWsUrl(opts.wsUrl);
|
|
22
|
-
this.agentId = opts.agentId;
|
|
23
|
-
this.apiKey = opts.apiKey;
|
|
24
|
-
this.join = opts.join;
|
|
25
|
-
this.reconnect = {
|
|
26
|
-
enabled: opts.reconnect?.enabled ?? DEFAULT_RECONNECT.enabled,
|
|
27
|
-
baseMs: opts.reconnect?.baseMs ?? DEFAULT_RECONNECT.baseMs,
|
|
28
|
-
maxMs: opts.reconnect?.maxMs ?? DEFAULT_RECONNECT.maxMs,
|
|
29
|
-
jitter: opts.reconnect?.jitter ?? DEFAULT_RECONNECT.jitter
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
async connect() {
|
|
33
|
-
this.shouldRun = true;
|
|
34
|
-
await this.openSocket();
|
|
35
|
-
await this.sendJoin();
|
|
36
|
-
}
|
|
37
|
-
async stop() {
|
|
38
|
-
this.shouldRun = false;
|
|
39
|
-
if (this.socket) {
|
|
40
|
-
this.socket.close();
|
|
41
|
-
this.socket = null;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
async sendAction(action) {
|
|
45
|
-
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
46
|
-
throw new Error("ws not connected");
|
|
47
|
-
}
|
|
48
|
-
this.socket.send(JSON.stringify(action));
|
|
49
|
-
}
|
|
50
|
-
async openSocket() {
|
|
51
|
-
const Impl = globalThis.WebSocket;
|
|
52
|
-
if (!Impl) {
|
|
53
|
-
throw new Error("WebSocket is unavailable in current Node runtime (requires Node 20+)");
|
|
54
|
-
}
|
|
55
|
-
await new Promise((resolve, reject) => {
|
|
56
|
-
const ws = new Impl(this.wsUrl);
|
|
57
|
-
this.socket = ws;
|
|
58
|
-
ws.onopen = () => {
|
|
59
|
-
this.connectAttempt = 0;
|
|
60
|
-
this.emit("connected");
|
|
61
|
-
resolve();
|
|
62
|
-
};
|
|
63
|
-
ws.onmessage = (evt) => {
|
|
64
|
-
try {
|
|
65
|
-
const data = JSON.parse(String(evt.data));
|
|
66
|
-
this.emit("event", data);
|
|
67
|
-
this.emit(data.type, data);
|
|
68
|
-
}
|
|
69
|
-
catch (err) {
|
|
70
|
-
this.emit("error", err);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
ws.onerror = () => {
|
|
74
|
-
reject(new Error("ws connection failed"));
|
|
75
|
-
};
|
|
76
|
-
ws.onclose = async () => {
|
|
77
|
-
this.emit("disconnected");
|
|
78
|
-
if (!this.shouldRun || !this.reconnect.enabled) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
await this.reconnectLoop();
|
|
82
|
-
};
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
async reconnectLoop() {
|
|
86
|
-
while (this.shouldRun) {
|
|
87
|
-
this.connectAttempt += 1;
|
|
88
|
-
const waitMs = computeBackoffMs(this.connectAttempt, this.reconnect.baseMs, this.reconnect.maxMs, this.reconnect.jitter);
|
|
89
|
-
this.emit("reconnect", { attempt: this.connectAttempt, waitMs });
|
|
90
|
-
await sleep(waitMs);
|
|
91
|
-
try {
|
|
92
|
-
await this.openSocket();
|
|
93
|
-
await this.sendJoin();
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
catch (err) {
|
|
97
|
-
this.emit("error", err);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
async sendJoin() {
|
|
102
|
-
const msg = {
|
|
103
|
-
type: "join",
|
|
104
|
-
agent_id: this.agentId,
|
|
105
|
-
api_key: this.apiKey,
|
|
106
|
-
join_mode: this.join.mode
|
|
107
|
-
};
|
|
108
|
-
if (this.join.mode === "select") {
|
|
109
|
-
msg.room_id = this.join.roomId;
|
|
110
|
-
}
|
|
111
|
-
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
112
|
-
throw new Error("ws not connected for join");
|
|
113
|
-
}
|
|
114
|
-
this.socket.send(JSON.stringify(msg));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|