@deeplake/hivemind 0.6.48 → 0.7.4
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +147 -20
- package/bundle/cli.js +552 -95
- package/codex/bundle/capture.js +509 -89
- package/codex/bundle/commands/auth-login.js +209 -66
- package/codex/bundle/embeddings/embed-daemon.js +243 -0
- package/codex/bundle/pre-tool-use.js +629 -104
- package/codex/bundle/session-start-setup.js +194 -57
- package/codex/bundle/session-start.js +25 -10
- package/codex/bundle/shell/deeplake-shell.js +679 -112
- package/codex/bundle/stop.js +476 -58
- package/codex/bundle/wiki-worker.js +312 -11
- package/cursor/bundle/capture.js +768 -57
- package/cursor/bundle/commands/auth-login.js +209 -66
- package/cursor/bundle/embeddings/embed-daemon.js +243 -0
- package/cursor/bundle/pre-tool-use.js +561 -70
- package/cursor/bundle/session-end.js +223 -2
- package/cursor/bundle/session-start.js +192 -54
- package/cursor/bundle/shell/deeplake-shell.js +679 -112
- package/cursor/bundle/wiki-worker.js +571 -0
- package/hermes/bundle/capture.js +771 -58
- package/hermes/bundle/commands/auth-login.js +209 -66
- package/hermes/bundle/embeddings/embed-daemon.js +243 -0
- package/hermes/bundle/pre-tool-use.js +560 -69
- package/hermes/bundle/session-end.js +224 -1
- package/hermes/bundle/session-start.js +195 -54
- package/hermes/bundle/shell/deeplake-shell.js +679 -112
- package/hermes/bundle/wiki-worker.js +572 -0
- package/mcp/bundle/server.js +253 -68
- package/openclaw/dist/chunks/auth-creds-AEKS6D3P.js +14 -0
- package/openclaw/dist/chunks/chunk-SRCBBT4H.js +37 -0
- package/openclaw/dist/chunks/config-G23NI5TV.js +33 -0
- package/openclaw/dist/chunks/index-marker-store-PGT5CW6T.js +33 -0
- package/openclaw/dist/chunks/setup-config-C35UK4LP.js +114 -0
- package/openclaw/dist/index.js +752 -702
- package/openclaw/openclaw.plugin.json +1 -1
- package/openclaw/package.json +1 -1
- package/package.json +2 -1
- package/pi/extension-source/hivemind.ts +473 -21
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// dist/src/hooks/codex/wiki-worker.js
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, appendFileSync as appendFileSync2, mkdirSync as mkdirSync2, rmSync } from "node:fs";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
|
-
import { join as
|
|
6
|
+
import { dirname, join as join5 } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
|
|
8
9
|
// dist/src/hooks/summary-state.js
|
|
9
10
|
import { readFileSync, writeFileSync, writeSync, mkdirSync, renameSync, existsSync, unlinkSync, openSync, closeSync } from "node:fs";
|
|
@@ -106,6 +107,21 @@ function releaseLock(sessionId) {
|
|
|
106
107
|
|
|
107
108
|
// dist/src/hooks/upload-summary.js
|
|
108
109
|
import { randomUUID } from "node:crypto";
|
|
110
|
+
|
|
111
|
+
// dist/src/embeddings/sql.js
|
|
112
|
+
function embeddingSqlLiteral(vec) {
|
|
113
|
+
if (!vec || vec.length === 0)
|
|
114
|
+
return "NULL";
|
|
115
|
+
const parts = [];
|
|
116
|
+
for (const v of vec) {
|
|
117
|
+
if (!Number.isFinite(v))
|
|
118
|
+
return "NULL";
|
|
119
|
+
parts.push(String(v));
|
|
120
|
+
}
|
|
121
|
+
return `ARRAY[${parts.join(",")}]::float4[]`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// dist/src/hooks/upload-summary.js
|
|
109
125
|
function esc(s) {
|
|
110
126
|
return s.replace(/\\/g, "\\\\").replace(/'/g, "''").replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
111
127
|
}
|
|
@@ -118,23 +134,297 @@ async function uploadSummary(query2, params) {
|
|
|
118
134
|
const ts = params.ts ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
119
135
|
const desc = extractDescription(text);
|
|
120
136
|
const sizeBytes = Buffer.byteLength(text);
|
|
137
|
+
const embSql = embeddingSqlLiteral(params.embedding ?? null);
|
|
121
138
|
const existing = await query2(`SELECT path FROM "${tableName}" WHERE path = '${esc(vpath)}' LIMIT 1`);
|
|
122
139
|
if (existing.length > 0) {
|
|
123
|
-
const sql2 = `UPDATE "${tableName}" SET summary = E'${esc(text)}', size_bytes = ${sizeBytes}, description = E'${esc(desc)}', last_update_date = '${ts}' WHERE path = '${esc(vpath)}'`;
|
|
140
|
+
const sql2 = `UPDATE "${tableName}" SET summary = E'${esc(text)}', summary_embedding = ${embSql}, size_bytes = ${sizeBytes}, description = E'${esc(desc)}', last_update_date = '${ts}' WHERE path = '${esc(vpath)}'`;
|
|
124
141
|
await query2(sql2);
|
|
125
142
|
return { path: "update", sql: sql2, descLength: desc.length, summaryLength: text.length };
|
|
126
143
|
}
|
|
127
|
-
const sql = `INSERT INTO "${tableName}" (id, path, filename, summary, author, mime_type, size_bytes, project, description, agent, creation_date, last_update_date) VALUES ('${randomUUID()}', '${esc(vpath)}', '${esc(fname)}', E'${esc(text)}', '${esc(userName)}', 'text/markdown', ${sizeBytes}, '${esc(project)}', E'${esc(desc)}', '${esc(agent)}', '${ts}', '${ts}')`;
|
|
144
|
+
const sql = `INSERT INTO "${tableName}" (id, path, filename, summary, summary_embedding, author, mime_type, size_bytes, project, description, agent, creation_date, last_update_date) VALUES ('${randomUUID()}', '${esc(vpath)}', '${esc(fname)}', E'${esc(text)}', ${embSql}, '${esc(userName)}', 'text/markdown', ${sizeBytes}, '${esc(project)}', E'${esc(desc)}', '${esc(agent)}', '${ts}', '${ts}')`;
|
|
128
145
|
await query2(sql);
|
|
129
146
|
return { path: "insert", sql, descLength: desc.length, summaryLength: text.length };
|
|
130
147
|
}
|
|
131
148
|
|
|
149
|
+
// dist/src/embeddings/client.js
|
|
150
|
+
import { connect } from "node:net";
|
|
151
|
+
import { spawn } from "node:child_process";
|
|
152
|
+
import { openSync as openSync2, closeSync as closeSync2, writeSync as writeSync2, unlinkSync as unlinkSync2, existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
153
|
+
import { homedir as homedir3 } from "node:os";
|
|
154
|
+
import { join as join3 } from "node:path";
|
|
155
|
+
|
|
156
|
+
// dist/src/embeddings/protocol.js
|
|
157
|
+
var DEFAULT_SOCKET_DIR = "/tmp";
|
|
158
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
159
|
+
var DEFAULT_CLIENT_TIMEOUT_MS = 2e3;
|
|
160
|
+
function socketPathFor(uid, dir = DEFAULT_SOCKET_DIR) {
|
|
161
|
+
return `${dir}/hivemind-embed-${uid}.sock`;
|
|
162
|
+
}
|
|
163
|
+
function pidPathFor(uid, dir = DEFAULT_SOCKET_DIR) {
|
|
164
|
+
return `${dir}/hivemind-embed-${uid}.pid`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// dist/src/embeddings/client.js
|
|
168
|
+
var SHARED_DAEMON_PATH = join3(homedir3(), ".hivemind", "embed-deps", "embed-daemon.js");
|
|
169
|
+
var log2 = (m) => log("embed-client", m);
|
|
170
|
+
function getUid() {
|
|
171
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
172
|
+
return uid !== void 0 ? String(uid) : process.env.USER ?? "default";
|
|
173
|
+
}
|
|
174
|
+
var EmbedClient = class {
|
|
175
|
+
socketPath;
|
|
176
|
+
pidPath;
|
|
177
|
+
timeoutMs;
|
|
178
|
+
daemonEntry;
|
|
179
|
+
autoSpawn;
|
|
180
|
+
spawnWaitMs;
|
|
181
|
+
nextId = 0;
|
|
182
|
+
constructor(opts = {}) {
|
|
183
|
+
const uid = getUid();
|
|
184
|
+
const dir = opts.socketDir ?? "/tmp";
|
|
185
|
+
this.socketPath = socketPathFor(uid, dir);
|
|
186
|
+
this.pidPath = pidPathFor(uid, dir);
|
|
187
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_CLIENT_TIMEOUT_MS;
|
|
188
|
+
this.daemonEntry = opts.daemonEntry ?? process.env.HIVEMIND_EMBED_DAEMON ?? (existsSync2(SHARED_DAEMON_PATH) ? SHARED_DAEMON_PATH : void 0);
|
|
189
|
+
this.autoSpawn = opts.autoSpawn ?? true;
|
|
190
|
+
this.spawnWaitMs = opts.spawnWaitMs ?? 5e3;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Returns an embedding vector, or null on timeout/failure. Hooks MUST treat
|
|
194
|
+
* null as "skip embedding column" — never block the write path on us.
|
|
195
|
+
*
|
|
196
|
+
* Fire-and-forget spawn on miss: if the daemon isn't up, this call returns
|
|
197
|
+
* null AND kicks off a background spawn. The next call finds a ready daemon.
|
|
198
|
+
*/
|
|
199
|
+
async embed(text, kind = "document") {
|
|
200
|
+
let sock;
|
|
201
|
+
try {
|
|
202
|
+
sock = await this.connectOnce();
|
|
203
|
+
} catch {
|
|
204
|
+
if (this.autoSpawn)
|
|
205
|
+
this.trySpawnDaemon();
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const id = String(++this.nextId);
|
|
210
|
+
const req = { op: "embed", id, kind, text };
|
|
211
|
+
const resp = await this.sendAndWait(sock, req);
|
|
212
|
+
if (resp.error || !("embedding" in resp) || !resp.embedding) {
|
|
213
|
+
log2(`embed err: ${resp.error ?? "no embedding"}`);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return resp.embedding;
|
|
217
|
+
} catch (e) {
|
|
218
|
+
const err = e instanceof Error ? e.message : String(e);
|
|
219
|
+
log2(`embed failed: ${err}`);
|
|
220
|
+
return null;
|
|
221
|
+
} finally {
|
|
222
|
+
try {
|
|
223
|
+
sock.end();
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Wait up to spawnWaitMs for the daemon to accept connections, spawning if
|
|
230
|
+
* necessary. Meant for SessionStart / long-running batches — not the hot path.
|
|
231
|
+
*/
|
|
232
|
+
async warmup() {
|
|
233
|
+
try {
|
|
234
|
+
const s = await this.connectOnce();
|
|
235
|
+
s.end();
|
|
236
|
+
return true;
|
|
237
|
+
} catch {
|
|
238
|
+
if (!this.autoSpawn)
|
|
239
|
+
return false;
|
|
240
|
+
this.trySpawnDaemon();
|
|
241
|
+
try {
|
|
242
|
+
const s = await this.waitForSocket();
|
|
243
|
+
s.end();
|
|
244
|
+
return true;
|
|
245
|
+
} catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
connectOnce() {
|
|
251
|
+
return new Promise((resolve, reject) => {
|
|
252
|
+
const sock = connect(this.socketPath);
|
|
253
|
+
const to = setTimeout(() => {
|
|
254
|
+
sock.destroy();
|
|
255
|
+
reject(new Error("connect timeout"));
|
|
256
|
+
}, this.timeoutMs);
|
|
257
|
+
sock.once("connect", () => {
|
|
258
|
+
clearTimeout(to);
|
|
259
|
+
resolve(sock);
|
|
260
|
+
});
|
|
261
|
+
sock.once("error", (e) => {
|
|
262
|
+
clearTimeout(to);
|
|
263
|
+
reject(e);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
trySpawnDaemon() {
|
|
268
|
+
let fd;
|
|
269
|
+
try {
|
|
270
|
+
fd = openSync2(this.pidPath, "wx", 384);
|
|
271
|
+
writeSync2(fd, String(process.pid));
|
|
272
|
+
} catch (e) {
|
|
273
|
+
if (this.isPidFileStale()) {
|
|
274
|
+
try {
|
|
275
|
+
unlinkSync2(this.pidPath);
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
fd = openSync2(this.pidPath, "wx", 384);
|
|
280
|
+
writeSync2(fd, String(process.pid));
|
|
281
|
+
} catch {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (!this.daemonEntry || !existsSync2(this.daemonEntry)) {
|
|
289
|
+
log2(`daemonEntry not configured or missing: ${this.daemonEntry}`);
|
|
290
|
+
try {
|
|
291
|
+
closeSync2(fd);
|
|
292
|
+
unlinkSync2(this.pidPath);
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const child = spawn(process.execPath, [this.daemonEntry], {
|
|
299
|
+
detached: true,
|
|
300
|
+
stdio: "ignore",
|
|
301
|
+
env: process.env
|
|
302
|
+
});
|
|
303
|
+
child.unref();
|
|
304
|
+
log2(`spawned daemon pid=${child.pid}`);
|
|
305
|
+
} finally {
|
|
306
|
+
closeSync2(fd);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
isPidFileStale() {
|
|
310
|
+
try {
|
|
311
|
+
const raw = readFileSync2(this.pidPath, "utf-8").trim();
|
|
312
|
+
const pid = Number(raw);
|
|
313
|
+
if (!pid || Number.isNaN(pid))
|
|
314
|
+
return true;
|
|
315
|
+
try {
|
|
316
|
+
process.kill(pid, 0);
|
|
317
|
+
return false;
|
|
318
|
+
} catch {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async waitForSocket() {
|
|
326
|
+
const deadline = Date.now() + this.spawnWaitMs;
|
|
327
|
+
let delay = 30;
|
|
328
|
+
while (Date.now() < deadline) {
|
|
329
|
+
await sleep(delay);
|
|
330
|
+
delay = Math.min(delay * 1.5, 300);
|
|
331
|
+
if (!existsSync2(this.socketPath))
|
|
332
|
+
continue;
|
|
333
|
+
try {
|
|
334
|
+
return await this.connectOnce();
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
throw new Error("daemon did not become ready within spawnWaitMs");
|
|
339
|
+
}
|
|
340
|
+
sendAndWait(sock, req) {
|
|
341
|
+
return new Promise((resolve, reject) => {
|
|
342
|
+
let buf = "";
|
|
343
|
+
const to = setTimeout(() => {
|
|
344
|
+
sock.destroy();
|
|
345
|
+
reject(new Error("request timeout"));
|
|
346
|
+
}, this.timeoutMs);
|
|
347
|
+
sock.setEncoding("utf-8");
|
|
348
|
+
sock.on("data", (chunk) => {
|
|
349
|
+
buf += chunk;
|
|
350
|
+
const nl = buf.indexOf("\n");
|
|
351
|
+
if (nl === -1)
|
|
352
|
+
return;
|
|
353
|
+
const line = buf.slice(0, nl);
|
|
354
|
+
clearTimeout(to);
|
|
355
|
+
try {
|
|
356
|
+
resolve(JSON.parse(line));
|
|
357
|
+
} catch (e) {
|
|
358
|
+
reject(e);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
sock.on("error", (e) => {
|
|
362
|
+
clearTimeout(to);
|
|
363
|
+
reject(e);
|
|
364
|
+
});
|
|
365
|
+
sock.on("end", () => {
|
|
366
|
+
clearTimeout(to);
|
|
367
|
+
reject(new Error("connection closed without response"));
|
|
368
|
+
});
|
|
369
|
+
sock.write(JSON.stringify(req) + "\n");
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
function sleep(ms) {
|
|
374
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// dist/src/embeddings/disable.js
|
|
378
|
+
import { createRequire } from "node:module";
|
|
379
|
+
import { homedir as homedir4 } from "node:os";
|
|
380
|
+
import { join as join4 } from "node:path";
|
|
381
|
+
import { pathToFileURL } from "node:url";
|
|
382
|
+
var cachedStatus = null;
|
|
383
|
+
function defaultResolveTransformers() {
|
|
384
|
+
try {
|
|
385
|
+
createRequire(import.meta.url).resolve("@huggingface/transformers");
|
|
386
|
+
return;
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
const sharedDir = join4(homedir4(), ".hivemind", "embed-deps");
|
|
390
|
+
createRequire(pathToFileURL(`${sharedDir}/`).href).resolve("@huggingface/transformers");
|
|
391
|
+
}
|
|
392
|
+
var _resolve = defaultResolveTransformers;
|
|
393
|
+
function detectStatus() {
|
|
394
|
+
if (process.env.HIVEMIND_EMBEDDINGS === "false")
|
|
395
|
+
return "env-disabled";
|
|
396
|
+
try {
|
|
397
|
+
_resolve();
|
|
398
|
+
return "enabled";
|
|
399
|
+
} catch {
|
|
400
|
+
return "no-transformers";
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function embeddingsStatus() {
|
|
404
|
+
if (cachedStatus !== null)
|
|
405
|
+
return cachedStatus;
|
|
406
|
+
cachedStatus = detectStatus();
|
|
407
|
+
return cachedStatus;
|
|
408
|
+
}
|
|
409
|
+
function embeddingsDisabled() {
|
|
410
|
+
return embeddingsStatus() !== "enabled";
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// dist/src/utils/client-header.js
|
|
414
|
+
var DEEPLAKE_CLIENT_HEADER = "X-Deeplake-Client";
|
|
415
|
+
function deeplakeClientValue() {
|
|
416
|
+
return "hivemind";
|
|
417
|
+
}
|
|
418
|
+
function deeplakeClientHeader() {
|
|
419
|
+
return { [DEEPLAKE_CLIENT_HEADER]: deeplakeClientValue() };
|
|
420
|
+
}
|
|
421
|
+
|
|
132
422
|
// dist/src/hooks/codex/wiki-worker.js
|
|
133
423
|
var dlog2 = (msg) => log("codex-wiki-worker", msg);
|
|
134
|
-
var cfg = JSON.parse(
|
|
424
|
+
var cfg = JSON.parse(readFileSync3(process.argv[2], "utf-8"));
|
|
135
425
|
var tmpDir = cfg.tmpDir;
|
|
136
|
-
var tmpJsonl =
|
|
137
|
-
var tmpSummary =
|
|
426
|
+
var tmpJsonl = join5(tmpDir, "session.jsonl");
|
|
427
|
+
var tmpSummary = join5(tmpDir, "summary.md");
|
|
138
428
|
function wlog(msg) {
|
|
139
429
|
try {
|
|
140
430
|
mkdirSync2(cfg.hooksDir, { recursive: true });
|
|
@@ -153,7 +443,8 @@ async function query(sql, retries = 4) {
|
|
|
153
443
|
headers: {
|
|
154
444
|
Authorization: `Bearer ${cfg.token}`,
|
|
155
445
|
"Content-Type": "application/json",
|
|
156
|
-
"X-Activeloop-Org-Id": cfg.orgId
|
|
446
|
+
"X-Activeloop-Org-Id": cfg.orgId,
|
|
447
|
+
...deeplakeClientHeader()
|
|
157
448
|
},
|
|
158
449
|
body: JSON.stringify({ query: sql })
|
|
159
450
|
});
|
|
@@ -225,11 +516,20 @@ async function main() {
|
|
|
225
516
|
} catch (e) {
|
|
226
517
|
wlog(`codex exec failed: ${e.status ?? e.message}`);
|
|
227
518
|
}
|
|
228
|
-
if (
|
|
229
|
-
const text =
|
|
519
|
+
if (existsSync3(tmpSummary)) {
|
|
520
|
+
const text = readFileSync3(tmpSummary, "utf-8");
|
|
230
521
|
if (text.trim()) {
|
|
231
522
|
const fname = `${cfg.sessionId}.md`;
|
|
232
523
|
const vpath = `/summaries/${cfg.userName}/${fname}`;
|
|
524
|
+
let embedding = null;
|
|
525
|
+
if (!embeddingsDisabled()) {
|
|
526
|
+
try {
|
|
527
|
+
const daemonEntry = join5(dirname(fileURLToPath(import.meta.url)), "embeddings", "embed-daemon.js");
|
|
528
|
+
embedding = await new EmbedClient({ daemonEntry }).embed(text, "document");
|
|
529
|
+
} catch (e) {
|
|
530
|
+
wlog(`summary embedding failed, writing NULL: ${e.message}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
233
533
|
const result = await uploadSummary(query, {
|
|
234
534
|
tableName: cfg.memoryTable,
|
|
235
535
|
vpath,
|
|
@@ -238,7 +538,8 @@ async function main() {
|
|
|
238
538
|
project: cfg.project,
|
|
239
539
|
agent: "codex",
|
|
240
540
|
sessionId: cfg.sessionId,
|
|
241
|
-
text
|
|
541
|
+
text,
|
|
542
|
+
embedding
|
|
242
543
|
});
|
|
243
544
|
wlog(`uploaded ${vpath} (summary=${result.summaryLength}, desc=${result.descLength})`);
|
|
244
545
|
try {
|