@opencomputer/blue 0.0.9 → 0.0.15
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 +5 -0
- package/dist/channels/slack-api.js +3 -2
- package/dist/channels/slack-api.js.map +1 -1
- package/dist/channels/slack-format.d.ts +1 -0
- package/dist/channels/slack-format.js +34 -0
- package/dist/channels/slack-format.js.map +1 -0
- package/dist/channels/slack-project.js +6 -1
- package/dist/channels/slack-project.js.map +1 -1
- package/dist/cli/index.js +89 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/core/agent.d.ts +1 -0
- package/dist/core/agent.js +90 -7
- package/dist/core/agent.js.map +1 -1
- package/dist/dev/local-control-plane.js +3 -1
- package/dist/dev/local-control-plane.js.map +1 -1
- package/dist/dev/manager.js +120 -6
- package/dist/dev/manager.js.map +1 -1
- package/dist/landing/dev-session.d.ts +5 -0
- package/dist/landing/dev-session.js +323 -0
- package/dist/landing/dev-session.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
function escapeHtml(value) {
|
|
2
|
+
return value
|
|
3
|
+
.replace(/&/g, "&")
|
|
4
|
+
.replace(/</g, "<")
|
|
5
|
+
.replace(/>/g, ">")
|
|
6
|
+
.replace(/"/g, """)
|
|
7
|
+
.replace(/'/g, "'");
|
|
8
|
+
}
|
|
9
|
+
export function renderDevSessionLanding(options) {
|
|
10
|
+
const agentId = escapeHtml(options.agentId);
|
|
11
|
+
const nonce = escapeHtml(options.nonce);
|
|
12
|
+
return `<!doctype html>
|
|
13
|
+
<html lang="en">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8">
|
|
16
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
17
|
+
<meta name="color-scheme" content="light">
|
|
18
|
+
<title>${agentId} | Blue dev</title>
|
|
19
|
+
<style>
|
|
20
|
+
:root { font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #17202a; background: #f7f8fa; }
|
|
21
|
+
* { box-sizing: border-box; }
|
|
22
|
+
body { margin: 0; min-height: 100vh; }
|
|
23
|
+
button, textarea { font: inherit; }
|
|
24
|
+
button { cursor: pointer; }
|
|
25
|
+
header { height: 58px; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 0 22px; border-bottom: 1px solid #dfe3e8; background: #fff; }
|
|
26
|
+
.brand { display: flex; align-items: center; gap: 10px; min-width: 0; font-weight: 750; }
|
|
27
|
+
.mark { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 6px; color: #fff; background: #1447e6; }
|
|
28
|
+
.agent { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
29
|
+
.status { display: flex; align-items: center; gap: 7px; color: #52606d; font-size: 13px; }
|
|
30
|
+
.dot { width: 8px; height: 8px; border-radius: 50%; background: #238636; }
|
|
31
|
+
.shell { display: grid; grid-template-columns: 280px minmax(0, 1fr); height: calc(100vh - 58px); }
|
|
32
|
+
aside { min-width: 0; padding: 16px 12px; border-right: 1px solid #dfe3e8; background: #f1f3f5; overflow-y: auto; }
|
|
33
|
+
.new { width: 100%; height: 38px; border: 0; border-radius: 6px; color: #fff; background: #1447e6; font-weight: 700; }
|
|
34
|
+
.new:hover { background: #103bbd; }
|
|
35
|
+
.section-label { margin: 20px 8px 8px; color: #687585; font-size: 11px; font-weight: 750; text-transform: uppercase; }
|
|
36
|
+
.session { width: 100%; min-height: 54px; margin: 2px 0; padding: 9px 10px; border: 0; border-radius: 6px; text-align: left; color: #25313c; background: transparent; }
|
|
37
|
+
.session:hover, .session.active { background: #fff; box-shadow: inset 0 0 0 1px #d8dde5; }
|
|
38
|
+
.session strong, .session span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
39
|
+
.session strong { font-size: 13px; }
|
|
40
|
+
.session span { margin-top: 4px; color: #718096; font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
41
|
+
main { display: grid; grid-template-rows: 52px minmax(0, 1fr) auto; min-width: 0; background: #fff; }
|
|
42
|
+
.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 20px; border-bottom: 1px solid #e6e9ed; }
|
|
43
|
+
.toolbar h1 { margin: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 15px; }
|
|
44
|
+
.end { border: 1px solid #d6dbe2; border-radius: 6px; padding: 7px 10px; color: #9b2c2c; background: #fff; font-size: 12px; font-weight: 700; }
|
|
45
|
+
.end:disabled { opacity: .45; cursor: default; }
|
|
46
|
+
.timeline { padding: 24px max(24px, calc((100% - 820px) / 2)); overflow-y: auto; }
|
|
47
|
+
.empty { display: grid; place-content: center; height: 100%; color: #687585; text-align: center; }
|
|
48
|
+
.empty strong { color: #25313c; font-size: 18px; }
|
|
49
|
+
.empty p { margin: 8px 0 0; line-height: 1.55; }
|
|
50
|
+
.message { margin: 0 0 22px; }
|
|
51
|
+
.message .label { margin-bottom: 7px; color: #667382; font-size: 11px; font-weight: 750; text-transform: uppercase; }
|
|
52
|
+
.message .body { line-height: 1.65; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
53
|
+
.message.user .body { display: inline-block; max-width: 86%; padding: 10px 13px; border-radius: 7px; background: #eef2ff; }
|
|
54
|
+
.reasoning { margin: 8px 0 18px; padding-left: 12px; border-left: 2px solid #d1d7df; color: #697685; font-size: 13px; line-height: 1.55; white-space: pre-wrap; }
|
|
55
|
+
.tool { display: flex; align-items: center; gap: 9px; margin: 8px 0; color: #52606d; font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
56
|
+
.tool i { width: 8px; height: 8px; border-radius: 50%; background: #d29922; }
|
|
57
|
+
.tool.done i { background: #238636; }
|
|
58
|
+
.tool.failed i { background: #cf222e; }
|
|
59
|
+
form { display: flex; gap: 10px; padding: 14px max(20px, calc((100% - 840px) / 2)) 18px; border-top: 1px solid #e6e9ed; background: #fff; }
|
|
60
|
+
textarea { width: 100%; min-height: 48px; max-height: 160px; resize: vertical; border: 1px solid #cfd6df; border-radius: 7px; padding: 12px; outline: none; line-height: 1.45; }
|
|
61
|
+
textarea:focus { border-color: #1447e6; box-shadow: 0 0 0 2px rgba(20,71,230,.12); }
|
|
62
|
+
.send { align-self: end; height: 42px; border: 0; border-radius: 6px; padding: 0 17px; color: #fff; background: #1447e6; font-weight: 700; }
|
|
63
|
+
.send:disabled { opacity: .5; cursor: default; }
|
|
64
|
+
.error { color: #b42318; }
|
|
65
|
+
@media (max-width: 720px) {
|
|
66
|
+
.shell { grid-template-columns: 1fr; }
|
|
67
|
+
aside { display: none; }
|
|
68
|
+
.timeline { padding: 20px 16px; }
|
|
69
|
+
form { padding: 12px; }
|
|
70
|
+
}
|
|
71
|
+
</style>
|
|
72
|
+
</head>
|
|
73
|
+
<body>
|
|
74
|
+
<header>
|
|
75
|
+
<div class="brand"><span class="mark">b</span><span class="agent">${agentId}</span></div>
|
|
76
|
+
<div class="status"><span class="dot"></span><span id="connection-status">Local control plane</span></div>
|
|
77
|
+
</header>
|
|
78
|
+
<div class="shell">
|
|
79
|
+
<aside>
|
|
80
|
+
<button class="new" id="new-session">New session</button>
|
|
81
|
+
<div class="section-label">Sessions</div>
|
|
82
|
+
<div id="sessions"></div>
|
|
83
|
+
</aside>
|
|
84
|
+
<main>
|
|
85
|
+
<div class="toolbar">
|
|
86
|
+
<h1 id="session-title">Development session</h1>
|
|
87
|
+
<button class="end" id="end-session" disabled>End session</button>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="timeline" id="timeline">
|
|
90
|
+
<div class="empty"><div><strong>Start a local session</strong><p>Test this agent without configuring a channel.</p></div></div>
|
|
91
|
+
</div>
|
|
92
|
+
<form id="composer">
|
|
93
|
+
<textarea id="prompt" rows="2" placeholder="Message the agent" disabled></textarea>
|
|
94
|
+
<button class="send" id="send" disabled>Send</button>
|
|
95
|
+
</form>
|
|
96
|
+
</main>
|
|
97
|
+
</div>
|
|
98
|
+
<script nonce="${nonce}">
|
|
99
|
+
(function () {
|
|
100
|
+
"use strict";
|
|
101
|
+
var token = new URLSearchParams(location.hash.slice(1)).get("token");
|
|
102
|
+
var sessions = [];
|
|
103
|
+
var activeSession = null;
|
|
104
|
+
var socket = null;
|
|
105
|
+
var cursor = 0;
|
|
106
|
+
var messageNodes = new Map();
|
|
107
|
+
var reasoningNodes = new Map();
|
|
108
|
+
var toolNodes = new Map();
|
|
109
|
+
var sessionsNode = document.getElementById("sessions");
|
|
110
|
+
var timeline = document.getElementById("timeline");
|
|
111
|
+
var prompt = document.getElementById("prompt");
|
|
112
|
+
var send = document.getElementById("send");
|
|
113
|
+
var end = document.getElementById("end-session");
|
|
114
|
+
var title = document.getElementById("session-title");
|
|
115
|
+
var connectionStatus = document.getElementById("connection-status");
|
|
116
|
+
|
|
117
|
+
function api(path, init) {
|
|
118
|
+
init = init || {};
|
|
119
|
+
var headers = new Headers(init.headers || {});
|
|
120
|
+
headers.set("authorization", "Bearer " + token);
|
|
121
|
+
if (init.body) headers.set("content-type", "application/json");
|
|
122
|
+
return fetch(path, Object.assign({}, init, { headers: headers })).then(async function (response) {
|
|
123
|
+
var body = await response.json().catch(function () { return {}; });
|
|
124
|
+
if (!response.ok) throw new Error(body.message || "Request failed");
|
|
125
|
+
return body;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function empty(text, detail, error) {
|
|
130
|
+
timeline.innerHTML = "";
|
|
131
|
+
var wrapper = document.createElement("div");
|
|
132
|
+
wrapper.className = "empty" + (error ? " error" : "");
|
|
133
|
+
var inner = document.createElement("div");
|
|
134
|
+
var strong = document.createElement("strong");
|
|
135
|
+
var paragraph = document.createElement("p");
|
|
136
|
+
strong.textContent = text;
|
|
137
|
+
paragraph.textContent = detail;
|
|
138
|
+
inner.append(strong, paragraph);
|
|
139
|
+
wrapper.append(inner);
|
|
140
|
+
timeline.append(wrapper);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function renderSessions() {
|
|
144
|
+
sessionsNode.innerHTML = "";
|
|
145
|
+
sessions.forEach(function (session) {
|
|
146
|
+
var button = document.createElement("button");
|
|
147
|
+
var name = document.createElement("strong");
|
|
148
|
+
var meta = document.createElement("span");
|
|
149
|
+
button.className = "session" + (activeSession && activeSession.id === session.id ? " active" : "");
|
|
150
|
+
name.textContent = session.turns.length ? session.turns[session.turns.length - 1].input : "New session";
|
|
151
|
+
meta.textContent = session.status + " " + session.id.slice(0, 8);
|
|
152
|
+
button.append(name, meta);
|
|
153
|
+
button.addEventListener("click", function () { void selectSession(session.id); });
|
|
154
|
+
sessionsNode.append(button);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function appendMessage(role, text, key) {
|
|
159
|
+
var node = key ? messageNodes.get(key) : null;
|
|
160
|
+
if (!node) {
|
|
161
|
+
node = document.createElement("div");
|
|
162
|
+
node.className = "message " + role;
|
|
163
|
+
var label = document.createElement("div");
|
|
164
|
+
var body = document.createElement("div");
|
|
165
|
+
label.className = "label";
|
|
166
|
+
body.className = "body";
|
|
167
|
+
label.textContent = role === "user" ? "You" : "${agentId}";
|
|
168
|
+
node.append(label, body);
|
|
169
|
+
timeline.append(node);
|
|
170
|
+
if (key) messageNodes.set(key, node);
|
|
171
|
+
}
|
|
172
|
+
node.querySelector(".body").textContent = text;
|
|
173
|
+
timeline.scrollTop = timeline.scrollHeight;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderSnapshot(session, events) {
|
|
177
|
+
timeline.innerHTML = "";
|
|
178
|
+
messageNodes.clear();
|
|
179
|
+
reasoningNodes.clear();
|
|
180
|
+
toolNodes.clear();
|
|
181
|
+
session.turns.forEach(function (turn) {
|
|
182
|
+
appendMessage("user", turn.input, "user:" + turn.id);
|
|
183
|
+
events
|
|
184
|
+
.filter(function (event) { return event.turnId === turn.id; })
|
|
185
|
+
.forEach(function (event) { handleEvent(event); });
|
|
186
|
+
});
|
|
187
|
+
if (!session.turns.length) empty("Session ready", "Send the first message to this agent.");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function handleEvent(event) {
|
|
191
|
+
cursor = Math.max(cursor, event.seq || 0);
|
|
192
|
+
var turnId = event.turnId || "session";
|
|
193
|
+
var data = event.data || {};
|
|
194
|
+
if (event.type === "message.delta") {
|
|
195
|
+
var key = "assistant:" + turnId;
|
|
196
|
+
var node = messageNodes.get(key);
|
|
197
|
+
var previous = node ? node.querySelector(".body").textContent : "";
|
|
198
|
+
appendMessage("assistant", previous + String(data.text || ""), key);
|
|
199
|
+
} else if (event.type === "message.completed") {
|
|
200
|
+
var completedKey = "assistant:" + turnId;
|
|
201
|
+
if (!messageNodes.has(completedKey)) appendMessage("assistant", String(data.text || ""), completedKey);
|
|
202
|
+
} else if (event.type === "reasoning.delta" || event.type === "reasoning.completed") {
|
|
203
|
+
var reasoning = reasoningNodes.get(turnId);
|
|
204
|
+
if (!reasoning) {
|
|
205
|
+
reasoning = document.createElement("div");
|
|
206
|
+
reasoning.className = "reasoning";
|
|
207
|
+
timeline.append(reasoning);
|
|
208
|
+
reasoningNodes.set(turnId, reasoning);
|
|
209
|
+
}
|
|
210
|
+
if (event.type === "reasoning.delta") reasoning.textContent += String(data.text || "");
|
|
211
|
+
else if (!reasoning.textContent) reasoning.textContent = String(data.text || "Reasoning completed");
|
|
212
|
+
} else if (event.type === "tool.started" || event.type === "tool.completed" || event.type === "tool.failed") {
|
|
213
|
+
var toolKey = turnId + ":" + String(data.id || data.callId || data.name || event.id);
|
|
214
|
+
var tool = toolNodes.get(toolKey);
|
|
215
|
+
if (!tool) {
|
|
216
|
+
tool = document.createElement("div");
|
|
217
|
+
tool.className = "tool";
|
|
218
|
+
tool.append(document.createElement("i"), document.createElement("span"));
|
|
219
|
+
timeline.append(tool);
|
|
220
|
+
toolNodes.set(toolKey, tool);
|
|
221
|
+
}
|
|
222
|
+
tool.className = "tool" + (event.type === "tool.completed" ? " done" : event.type === "tool.failed" ? " failed" : "");
|
|
223
|
+
tool.querySelector("span").textContent = String(data.name || data.tool || "tool") + " " + event.type.slice(5);
|
|
224
|
+
} else if (event.type === "turn.completed" || event.type === "turn.failed") {
|
|
225
|
+
send.disabled = false;
|
|
226
|
+
prompt.disabled = false;
|
|
227
|
+
void refreshSessions();
|
|
228
|
+
}
|
|
229
|
+
timeline.scrollTop = timeline.scrollHeight;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function connect(sessionId, after) {
|
|
233
|
+
if (socket) socket.close();
|
|
234
|
+
var protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
235
|
+
var url = protocol + "//" + location.host + "/api/sessions/" + encodeURIComponent(sessionId) + "/connect?token=" + encodeURIComponent(token) + "&cursor=" + String(after || 0);
|
|
236
|
+
socket = new WebSocket(url);
|
|
237
|
+
socket.addEventListener("open", function () { connectionStatus.textContent = "Session connected"; });
|
|
238
|
+
socket.addEventListener("message", function (message) {
|
|
239
|
+
var frame = JSON.parse(message.data);
|
|
240
|
+
if (frame.type === "event") handleEvent(frame.event);
|
|
241
|
+
});
|
|
242
|
+
socket.addEventListener("close", function () { connectionStatus.textContent = "Local control plane"; });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function refreshSessions() {
|
|
246
|
+
var result = await api("/api/sessions");
|
|
247
|
+
sessions = result.sessions;
|
|
248
|
+
renderSessions();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function selectSession(sessionId) {
|
|
252
|
+
var result = await api("/api/sessions/" + encodeURIComponent(sessionId) + "/resume", { method: "POST" });
|
|
253
|
+
activeSession = result.session;
|
|
254
|
+
cursor = 0;
|
|
255
|
+
title.textContent = "Session " + sessionId.slice(0, 8);
|
|
256
|
+
prompt.disabled = activeSession.status === "ended";
|
|
257
|
+
send.disabled = prompt.disabled;
|
|
258
|
+
end.disabled = prompt.disabled;
|
|
259
|
+
renderSnapshot(result.session, result.events);
|
|
260
|
+
renderSessions();
|
|
261
|
+
connect(sessionId, cursor);
|
|
262
|
+
prompt.focus();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function createSession() {
|
|
266
|
+
empty("Starting runtime", "Preparing the local OpenCode session.");
|
|
267
|
+
var result = await api("/api/sessions", { method: "POST", body: "{}" });
|
|
268
|
+
await refreshSessions();
|
|
269
|
+
await selectSession(result.session.id);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
document.getElementById("new-session").addEventListener("click", function () {
|
|
273
|
+
void createSession().catch(showError);
|
|
274
|
+
});
|
|
275
|
+
document.getElementById("composer").addEventListener("submit", function (event) {
|
|
276
|
+
event.preventDefault();
|
|
277
|
+
var input = prompt.value.trim();
|
|
278
|
+
if (!input || !activeSession || !socket || socket.readyState !== WebSocket.OPEN) return;
|
|
279
|
+
appendMessage("user", input, "user:pending:" + crypto.randomUUID());
|
|
280
|
+
socket.send(JSON.stringify({ type: "turn.create", input: input, idempotencyKey: crypto.randomUUID() }));
|
|
281
|
+
prompt.value = "";
|
|
282
|
+
prompt.disabled = true;
|
|
283
|
+
send.disabled = true;
|
|
284
|
+
});
|
|
285
|
+
prompt.addEventListener("keydown", function (event) {
|
|
286
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
287
|
+
event.preventDefault();
|
|
288
|
+
document.getElementById("composer").requestSubmit();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
end.addEventListener("click", function () {
|
|
292
|
+
if (!activeSession) return;
|
|
293
|
+
void api("/api/sessions/" + encodeURIComponent(activeSession.id), { method: "DELETE" })
|
|
294
|
+
.then(function () {
|
|
295
|
+
if (socket) socket.close();
|
|
296
|
+
activeSession = null;
|
|
297
|
+
prompt.disabled = true;
|
|
298
|
+
send.disabled = true;
|
|
299
|
+
end.disabled = true;
|
|
300
|
+
title.textContent = "Development session";
|
|
301
|
+
empty("Session ended", "Create another session when you are ready.");
|
|
302
|
+
return refreshSessions();
|
|
303
|
+
})
|
|
304
|
+
.catch(showError);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
function showError(error) {
|
|
308
|
+
empty("Could not use Blue dev", error instanceof Error ? error.message : String(error), true);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!token) {
|
|
312
|
+
showError(new Error("The dev token is missing. Open the web URL printed by blue dev."));
|
|
313
|
+
} else {
|
|
314
|
+
api("/api/config")
|
|
315
|
+
.then(function () { return refreshSessions(); })
|
|
316
|
+
.catch(showError);
|
|
317
|
+
}
|
|
318
|
+
})();
|
|
319
|
+
</script>
|
|
320
|
+
</body>
|
|
321
|
+
</html>`;
|
|
322
|
+
}
|
|
323
|
+
//# sourceMappingURL=dev-session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-session.js","sourceRoot":"","sources":["../../src/landing/dev-session.ts"],"names":[],"mappings":"AAKA,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,OAAiC;IAEjC,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACxC,OAAO;;;;;;WAME,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wEAyDsD,OAAO;;;;;;;;;;;;;;;;;;;;;;;mBAuB5D,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2DAqEmC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA0J1D,CAAC;AACT,CAAC"}
|