@andreprado/agentkit 0.1.0-alpha.4 → 0.1.0-alpha.6
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/docs/guides/add-channel.md +25 -0
- package/docs/guides/add-knowledge.md +134 -0
- package/docs/guides/agentkit-skills-architecture.md +471 -0
- package/docs/guides/channels-production-handoff.md +2 -0
- package/docs/guides/connect-telegram.md +15 -0
- package/docs/guides/connect-whatsapp-zapster.md +16 -0
- package/docs/guides/create-agent.md +1 -1
- package/docs/llms-full.txt +90 -1
- package/docs/llms.txt +9 -2
- package/package.json +2 -1
- package/src/cli/args.ts +36 -0
- package/src/cli/cloud-client.ts +257 -0
- package/src/cli/commands/channels.ts +459 -0
- package/src/cli/commands/knowledge.ts +136 -0
- package/src/cli/constants.ts +4 -0
- package/src/cli/deploy-chat-ui.ts +385 -0
- package/src/cli/deploy-readiness.ts +348 -0
- package/src/cli/flags.ts +162 -0
- package/src/cli/help.ts +166 -0
- package/src/cli/index.ts +219 -1912
- package/src/cli/process.ts +31 -0
- package/src/cloud/artifact.ts +92 -1
- package/src/cloud/contracts.ts +16 -0
- package/src/create-project.ts +38 -6
- package/src/index.ts +98 -0
- package/src/runtime/channel-buffer.ts +30 -0
- package/src/runtime/channels.ts +1 -0
- package/src/runtime/chat.ts +21 -2
- package/src/runtime/config.ts +167 -0
- package/src/runtime/core/manifest.ts +37 -0
- package/src/runtime/deploy-readiness.ts +12 -0
- package/src/runtime/dev-server.ts +159 -10
- package/src/runtime/inspect.ts +34 -0
- package/src/runtime/knowledge/chunk.ts +333 -0
- package/src/runtime/knowledge/config.ts +135 -0
- package/src/runtime/knowledge/embeddings.ts +133 -0
- package/src/runtime/knowledge/ingest.ts +521 -0
- package/src/runtime/knowledge/prompt-policy.ts +30 -0
- package/src/runtime/knowledge/retrieve.ts +283 -0
- package/src/runtime/knowledge/schema.ts +56 -0
- package/src/runtime/knowledge/tool.ts +64 -0
- package/src/runtime/knowledge/vector.ts +258 -0
- package/src/runtime/targets/cloudflare/build.ts +469 -4
- package/src/storage/sqlite.ts +5 -0
- package/src/templates/blank.ts +8 -4
- package/src/templates/dentista.ts +3 -1
- package/src/templates/skills/agentkit-build-agent/SKILL.md +49 -0
- package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +20 -0
- package/src/templates/skills/agentkit-build-agent/templates/sales-qualifier.instructions.md +17 -0
- package/src/templates/skills/agentkit-build-agent/templates/support-agent.instructions.md +16 -0
- package/src/templates/skills/agentkit-capsule/SKILL.md +62 -0
- package/src/templates/skills/agentkit-capsule/references/docs-router.md +15 -0
- package/src/templates/skills/agentkit-channels/SKILL.md +62 -0
- package/src/templates/skills/agentkit-channels/references/channel-buffering.md +58 -0
- package/src/templates/skills/agentkit-channels/references/channel-debugging.md +37 -0
- package/src/templates/skills/agentkit-channels/references/telegram.md +37 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +37 -0
- package/src/templates/skills/agentkit-database/SKILL.md +42 -0
- package/src/templates/skills/agentkit-database/templates/appointments.schema.sql +15 -0
- package/src/templates/skills/agentkit-database/templates/leads.schema.sql +17 -0
- package/src/templates/skills/agentkit-deploy/SKILL.md +44 -0
- package/src/templates/skills/agentkit-evals/SKILL.md +31 -0
- package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +14 -0
- package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +14 -0
- package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +18 -0
- package/src/templates/skills/agentkit-knowledge/SKILL.md +40 -0
- package/src/templates/skills/agentkit-knowledge/templates/faq.md +14 -0
- package/src/templates/skills/agentkit-knowledge/templates/policies.md +14 -0
- package/src/templates/skills/agentkit-knowledge/templates/prices.csv +3 -0
- package/src/templates/skills/agentkit-prompts/SKILL.md +45 -0
- package/src/templates/skills/agentkit-prompts/templates/knowledge-grounded-faq.instructions.md +11 -0
- package/src/templates/skills/agentkit-provider/SKILL.md +57 -0
- package/src/templates/skills/agentkit-security/SKILL.md +55 -0
- package/src/templates/skills/agentkit-tools/SKILL.md +36 -0
- package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +35 -0
- package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +37 -0
- package/src/templates/skills/agentkit-tools/examples/lookup-order.tool.md +46 -0
- package/src/templates/skills/agentkit-troubleshooting/SKILL.md +52 -0
- package/src/templates/support.ts +8 -4
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
3
|
+
import { join, relative, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { findAgentCapsuleRoot } from "../runtime/config";
|
|
6
|
+
import { readLocalDeployStateIfExists } from "../runtime/core/deploy-state";
|
|
7
|
+
import { AgentKitError, isAgentKitError } from "../runtime/errors";
|
|
8
|
+
import { defaultDeployChatAccessTokenName, defaultDeployChatAccessTokenPath } from "./constants";
|
|
9
|
+
|
|
10
|
+
export async function startDeployChatUiServer(
|
|
11
|
+
cwd: string,
|
|
12
|
+
options: { port: number; tokenFile?: string },
|
|
13
|
+
): Promise<{ url: string; deployUrl: string; tokenSource: string; stop(): Promise<void> }> {
|
|
14
|
+
const root = await findAgentCapsuleRoot(cwd);
|
|
15
|
+
const state = await readLocalDeployStateIfExists(root);
|
|
16
|
+
|
|
17
|
+
if (!state?.url) {
|
|
18
|
+
throw new Error("No hosted deploy URL found. Run agentkit deploy first.");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const tokenPath = options.tokenFile
|
|
22
|
+
? resolve(root, options.tokenFile)
|
|
23
|
+
: join(root, defaultDeployChatAccessTokenPath);
|
|
24
|
+
const hostname = "localhost";
|
|
25
|
+
const deployUrl = state.url;
|
|
26
|
+
const server = createServer(async (request, response) => {
|
|
27
|
+
try {
|
|
28
|
+
const requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? `${hostname}:${options.port}`}`);
|
|
29
|
+
|
|
30
|
+
if (request.method === "GET" && requestUrl.pathname === "/") {
|
|
31
|
+
sendHtml(response, renderDeployChatUi({ deployUrl }));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (request.method === "GET" && requestUrl.pathname === "/health") {
|
|
36
|
+
sendJson(response, 200, { status: "ok", target: "deploy-chat-ui", deployUrl });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (request.method === "GET" && requestUrl.pathname === "/_agentkit") {
|
|
41
|
+
sendJson(response, 200, {
|
|
42
|
+
deployUrl,
|
|
43
|
+
access: "proxied",
|
|
44
|
+
tokenSource: process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN ? "AGENTKIT_DEPLOY_ACCESS_TOKEN" : relativePath(root, tokenPath),
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (request.method === "POST" && (requestUrl.pathname === "/v1/chat" || requestUrl.pathname === "/chat")) {
|
|
50
|
+
const token = await readDeployAccessTokenForUi(tokenPath);
|
|
51
|
+
const body = await readIncomingBody(request);
|
|
52
|
+
const contentTypeHeader = request.headers["content-type"];
|
|
53
|
+
const contentType = Array.isArray(contentTypeHeader)
|
|
54
|
+
? contentTypeHeader[0] ?? "application/json"
|
|
55
|
+
: contentTypeHeader ?? "application/json";
|
|
56
|
+
const upstream = await fetch(new URL("/v1/chat", deployUrl).href, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
"Authorization": `Bearer ${token}`,
|
|
60
|
+
"Content-Type": contentType,
|
|
61
|
+
},
|
|
62
|
+
body,
|
|
63
|
+
});
|
|
64
|
+
const text = await upstream.text();
|
|
65
|
+
|
|
66
|
+
response.writeHead(upstream.status, {
|
|
67
|
+
"content-type": upstream.headers.get("content-type") ?? "application/json",
|
|
68
|
+
});
|
|
69
|
+
response.end(text);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
sendJson(response, 404, { error: { code: "not_found", message: "Route not found." } });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
sendJson(response, 500, {
|
|
76
|
+
error: {
|
|
77
|
+
code: isAgentKitError(error) ? error.code : "deploy_chat_ui_error",
|
|
78
|
+
message: error instanceof Error ? error.message : String(error),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await new Promise<void>((resolvePromise, reject) => {
|
|
85
|
+
server.once("error", reject);
|
|
86
|
+
server.listen(options.port, hostname, () => {
|
|
87
|
+
server.off("error", reject);
|
|
88
|
+
resolvePromise();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
url: `http://${hostname}:${options.port}`,
|
|
94
|
+
deployUrl,
|
|
95
|
+
tokenSource: process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN ? "AGENTKIT_DEPLOY_ACCESS_TOKEN" : relativePath(root, tokenPath),
|
|
96
|
+
stop: () =>
|
|
97
|
+
new Promise<void>((resolvePromise, reject) => {
|
|
98
|
+
server.close((error) => (error ? reject(error) : resolvePromise()));
|
|
99
|
+
}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function readDeployAccessTokenForUi(tokenPath: string): Promise<string> {
|
|
104
|
+
if (process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN) {
|
|
105
|
+
return process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let payload: unknown;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
payload = JSON.parse(await readFile(tokenPath, "utf8"));
|
|
112
|
+
} catch {
|
|
113
|
+
throw new AgentKitError(
|
|
114
|
+
"deploy_access_token_missing",
|
|
115
|
+
`Could not read a clean deploy access token file at ${tokenPath}. Run: agentkit access token create ${defaultDeployChatAccessTokenName} --out ${defaultDeployChatAccessTokenPath}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const token =
|
|
120
|
+
payload && typeof payload === "object"
|
|
121
|
+
? (payload as { access_token?: { token?: unknown }; token?: unknown; accessToken?: unknown }).access_token?.token ??
|
|
122
|
+
(payload as { token?: unknown }).token ??
|
|
123
|
+
(payload as { accessToken?: unknown }).accessToken
|
|
124
|
+
: undefined;
|
|
125
|
+
|
|
126
|
+
if (typeof token !== "string" || token.trim().length === 0) {
|
|
127
|
+
throw new AgentKitError(
|
|
128
|
+
"deploy_access_token_invalid",
|
|
129
|
+
`Deploy access token file ${tokenPath} does not contain access_token.token. Recreate it with: agentkit access token create ${defaultDeployChatAccessTokenName} --out ${defaultDeployChatAccessTokenPath}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return token.trim();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readIncomingBody(request: IncomingMessage): Promise<string> {
|
|
137
|
+
return new Promise((resolvePromise, reject) => {
|
|
138
|
+
let body = "";
|
|
139
|
+
request.setEncoding("utf8");
|
|
140
|
+
request.on("data", (chunk) => {
|
|
141
|
+
body += chunk;
|
|
142
|
+
});
|
|
143
|
+
request.on("end", () => resolvePromise(body));
|
|
144
|
+
request.on("error", reject);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function sendJson(response: ServerResponse, status: number, body: unknown): void {
|
|
149
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
150
|
+
response.end(JSON.stringify(body, null, 2));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sendHtml(response: ServerResponse, body: string): void {
|
|
154
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
155
|
+
response.end(body);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function renderDeployChatUi(input: { deployUrl: string }): string {
|
|
159
|
+
const deployUrl = escapeHtml(input.deployUrl);
|
|
160
|
+
|
|
161
|
+
return `<!doctype html>
|
|
162
|
+
<html lang="en">
|
|
163
|
+
<head>
|
|
164
|
+
<meta charset="utf-8">
|
|
165
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
166
|
+
<title>AgentKit Hosted Chat</title>
|
|
167
|
+
<style>
|
|
168
|
+
:root {
|
|
169
|
+
color-scheme: light;
|
|
170
|
+
--bg: #f6f7f5;
|
|
171
|
+
--panel: #ffffff;
|
|
172
|
+
--ink: #182022;
|
|
173
|
+
--muted: #667175;
|
|
174
|
+
--line: #d9ded9;
|
|
175
|
+
--accent: #176b5d;
|
|
176
|
+
--danger: #9f2d2d;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
* { box-sizing: border-box; }
|
|
180
|
+
|
|
181
|
+
body {
|
|
182
|
+
margin: 0;
|
|
183
|
+
min-height: 100svh;
|
|
184
|
+
background: var(--bg);
|
|
185
|
+
color: var(--ink);
|
|
186
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
main {
|
|
190
|
+
min-height: 100svh;
|
|
191
|
+
display: grid;
|
|
192
|
+
grid-template-rows: auto 1fr auto;
|
|
193
|
+
background: var(--panel);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
header {
|
|
197
|
+
border-bottom: 1px solid var(--line);
|
|
198
|
+
padding: 18px 22px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
h1 {
|
|
202
|
+
margin: 0 0 4px;
|
|
203
|
+
font-size: 20px;
|
|
204
|
+
line-height: 1.2;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
code {
|
|
208
|
+
color: var(--muted);
|
|
209
|
+
font-size: 12px;
|
|
210
|
+
overflow-wrap: anywhere;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#messages {
|
|
214
|
+
padding: 22px;
|
|
215
|
+
overflow: auto;
|
|
216
|
+
display: flex;
|
|
217
|
+
flex-direction: column;
|
|
218
|
+
gap: 12px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.message {
|
|
222
|
+
max-width: 780px;
|
|
223
|
+
display: grid;
|
|
224
|
+
gap: 4px;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.message.user {
|
|
228
|
+
align-self: end;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.role {
|
|
232
|
+
color: var(--muted);
|
|
233
|
+
font-size: 12px;
|
|
234
|
+
font-weight: 700;
|
|
235
|
+
text-transform: uppercase;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.bubble {
|
|
239
|
+
border: 1px solid var(--line);
|
|
240
|
+
border-radius: 8px;
|
|
241
|
+
padding: 12px 14px;
|
|
242
|
+
white-space: pre-wrap;
|
|
243
|
+
line-height: 1.45;
|
|
244
|
+
background: #fbfbfa;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.user .bubble {
|
|
248
|
+
border-color: #b8d8ce;
|
|
249
|
+
background: #e8f5f0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.error .bubble {
|
|
253
|
+
border-color: #e1b1b1;
|
|
254
|
+
color: var(--danger);
|
|
255
|
+
background: #fff6f6;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
form {
|
|
259
|
+
border-top: 1px solid var(--line);
|
|
260
|
+
padding: 14px;
|
|
261
|
+
display: grid;
|
|
262
|
+
grid-template-columns: 1fr auto;
|
|
263
|
+
gap: 10px;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
textarea {
|
|
267
|
+
min-height: 52px;
|
|
268
|
+
max-height: 180px;
|
|
269
|
+
resize: vertical;
|
|
270
|
+
border: 1px solid var(--line);
|
|
271
|
+
border-radius: 8px;
|
|
272
|
+
padding: 12px;
|
|
273
|
+
color: var(--ink);
|
|
274
|
+
font: inherit;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
button {
|
|
278
|
+
min-width: 96px;
|
|
279
|
+
border: 0;
|
|
280
|
+
border-radius: 8px;
|
|
281
|
+
padding: 0 18px;
|
|
282
|
+
color: white;
|
|
283
|
+
background: var(--accent);
|
|
284
|
+
font: inherit;
|
|
285
|
+
font-weight: 700;
|
|
286
|
+
cursor: pointer;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
button:disabled {
|
|
290
|
+
cursor: wait;
|
|
291
|
+
opacity: 0.65;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@media (max-width: 640px) {
|
|
295
|
+
form {
|
|
296
|
+
grid-template-columns: 1fr;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
button {
|
|
300
|
+
min-height: 44px;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
</style>
|
|
304
|
+
</head>
|
|
305
|
+
<body>
|
|
306
|
+
<main>
|
|
307
|
+
<header>
|
|
308
|
+
<h1>AgentKit Hosted Chat</h1>
|
|
309
|
+
<code>${deployUrl}</code>
|
|
310
|
+
</header>
|
|
311
|
+
<section id="messages" aria-live="polite"></section>
|
|
312
|
+
<form id="form">
|
|
313
|
+
<textarea id="message" name="message" placeholder="Type a message" required autofocus></textarea>
|
|
314
|
+
<button id="send" type="submit">Send</button>
|
|
315
|
+
</form>
|
|
316
|
+
</main>
|
|
317
|
+
<script>
|
|
318
|
+
const form = document.querySelector("#form");
|
|
319
|
+
const textarea = document.querySelector("#message");
|
|
320
|
+
const send = document.querySelector("#send");
|
|
321
|
+
const messages = document.querySelector("#messages");
|
|
322
|
+
let conversationId = null;
|
|
323
|
+
|
|
324
|
+
function addMessage(role, content, className = "") {
|
|
325
|
+
const wrapper = document.createElement("article");
|
|
326
|
+
wrapper.className = ["message", role, className].filter(Boolean).join(" ");
|
|
327
|
+
const label = document.createElement("div");
|
|
328
|
+
label.className = "role";
|
|
329
|
+
label.textContent = role;
|
|
330
|
+
const bubble = document.createElement("div");
|
|
331
|
+
bubble.className = "bubble";
|
|
332
|
+
bubble.textContent = content;
|
|
333
|
+
wrapper.append(label, bubble);
|
|
334
|
+
messages.append(wrapper);
|
|
335
|
+
messages.scrollTop = messages.scrollHeight;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
form.addEventListener("submit", async (event) => {
|
|
339
|
+
event.preventDefault();
|
|
340
|
+
const message = textarea.value.trim();
|
|
341
|
+
if (!message) return;
|
|
342
|
+
|
|
343
|
+
addMessage("user", message);
|
|
344
|
+
textarea.value = "";
|
|
345
|
+
send.disabled = true;
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const response = await fetch("/v1/chat", {
|
|
349
|
+
method: "POST",
|
|
350
|
+
headers: { "content-type": "application/json" },
|
|
351
|
+
body: JSON.stringify({ message, conversationId }),
|
|
352
|
+
});
|
|
353
|
+
const payload = await response.json();
|
|
354
|
+
|
|
355
|
+
if (!response.ok) {
|
|
356
|
+
throw new Error(payload.error?.message || "Request failed");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
conversationId = payload.conversationId || conversationId;
|
|
360
|
+
addMessage("assistant", payload.message?.content || "");
|
|
361
|
+
} catch (error) {
|
|
362
|
+
addMessage("error", error instanceof Error ? error.message : String(error), "error");
|
|
363
|
+
} finally {
|
|
364
|
+
send.disabled = false;
|
|
365
|
+
textarea.focus();
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
</script>
|
|
369
|
+
</body>
|
|
370
|
+
</html>`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function escapeHtml(value: string): string {
|
|
374
|
+
return value
|
|
375
|
+
.replace(/&/g, "&")
|
|
376
|
+
.replace(/</g, "<")
|
|
377
|
+
.replace(/>/g, ">")
|
|
378
|
+
.replace(/"/g, """)
|
|
379
|
+
.replace(/'/g, "'");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function relativePath(root: string, path: string): string {
|
|
383
|
+
const output = relative(root, path);
|
|
384
|
+
return output.startsWith("..") ? path : output || ".";
|
|
385
|
+
}
|