@vibe-cafe/vibe-usage 0.10.11 → 0.10.13
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 +1 -1
- package/package.json +1 -1
- package/src/parsers/claude-code.js +22 -8
- package/src/parsers/dsh.js +174 -65
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
|
|
|
69
69
|
| MiMoCode | `$MIMOCODE_HOME/data/mimocode.db`, `$XDG_DATA_HOME/mimocode/mimocode.db`, or `~/.local/share/mimocode/mimocode.db` (SQLite; exact input, output, reasoning, and cache-read tokens from assistant messages; honors `MIMOCODE_DB`; cache-write tokens are included in input usage) |
|
|
70
70
|
| Amp | `~/.local/share/amp/threads/`; cache-creation tokens are included in input usage |
|
|
71
71
|
| Droid | `~/.factory/sessions/` |
|
|
72
|
-
| DeepSeek Harness | `$DSH_HOME/sessions/` (default `~/.dsh`, fixture/relocation override: `VIBE_USAGE_DSH_SESSIONS`). Reads multi-frame Zstandard `session.jsonl.zstd` logs (built-in `node:zlib` zstd on Node ≥ 22.15, `zstd` CLI fallback) and plain `session.jsonl` logs. Usage comes from `assistant/message`: cache writes join uncached input, cache reads remain separate, and reasoning is split out of inclusive output.
|
|
72
|
+
| DeepSeek Harness | `$DSH_HOME/sessions/` (default `~/.dsh`, fixture/relocation override: `VIBE_USAGE_DSH_SESSIONS`). Reads multi-frame Zstandard `session.jsonl.zstd` logs (built-in `node:zlib` zstd on Node ≥ 22.15, `zstd` CLI fallback) and plain `session.jsonl` logs. Usage comes from `assistant/message`: cache writes join uncached input, cache reads remain separate, and reasoning is split out of inclusive output. Fork/subagent history is de-duplicated from the immutable header: `parentSession` identifies the source and `seedLength` gives the exact leading event boundary. Inherited messages are skipped only when matching source seqs remain in the parent file; missing parents fail open. `session/end-seed` positions are not used because resumes can append the marker after real history. |
|
|
73
73
|
| Hermes | `~/.hermes/state.db` + `~/.hermes/profiles/<name>/state.db` (SQLite, multi-profile) |
|
|
74
74
|
| Kiro | Kiro CLI native event streams `~/.kiro/sessions/cli/*.jsonl` (estimated tokens from message text: input = prompt + tool results, output = reply + tool calls, reasoning = thinking, cacheRead = re-sent context; thinking-block signatures excluded). Falls back to `~/Library/Application Support/kiro-cli/data.sqlite3` / `~/.local/share/kiro-cli/data.sqlite3` + optional `~/.kiro_sessions/*.json` archives, then IDE `q-client.log` whole-credit deltas as `kiro-credits` (floored cumulative diff — the server stores token counts as bigint); legacy IDE `dev_data/devdata.sqlite` token telemetry is opt-in with `VIBE_USAGE_KIRO_LEGACY_TOKENS=1` |
|
|
75
75
|
| Cline | Standalone `~/.cline/` plus `<host>/User/globalStorage/saoudrizwan.claude-dev/` across VSCode-fork hosts; migrated copies are deduplicated and empty leftover extension stores no longer count as installed |
|
package/package.json
CHANGED
|
@@ -195,7 +195,7 @@ async function scanProjectCandidate(candidate) {
|
|
|
195
195
|
if (usageScore === 0) return;
|
|
196
196
|
|
|
197
197
|
entries.push({
|
|
198
|
-
|
|
198
|
+
dedupeKey: usageDedupeKey(obj),
|
|
199
199
|
usageScore,
|
|
200
200
|
source: 'claude-code',
|
|
201
201
|
model,
|
|
@@ -239,22 +239,36 @@ async function scanBestCandidate(candidates, scanner, ctx) {
|
|
|
239
239
|
return null;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
// One API call is written as several assistant lines - one per content block -
|
|
243
|
+
// that share `message.id`/`requestId` and repeat the same `usage` object, so a
|
|
244
|
+
// per-line key counts the same call once per block. Streaming also emits an
|
|
245
|
+
// early partial line (lower `output_tokens`) before the final one under that
|
|
246
|
+
// same id. Keying on the call identity collapses both, and the existing
|
|
247
|
+
// highest-usageScore wins rule then keeps the final, complete payload.
|
|
248
|
+
// Records without either id (older logs) fall back to the line uuid.
|
|
249
|
+
function usageDedupeKey(obj) {
|
|
250
|
+
const messageId = typeof obj.message?.id === 'string' ? obj.message.id.trim() : '';
|
|
251
|
+
const requestId = typeof obj.requestId === 'string' ? obj.requestId.trim() : '';
|
|
252
|
+
if (messageId || requestId) return `call:${messageId}\u0000${requestId}`;
|
|
253
|
+
return typeof obj.uuid === 'string' && obj.uuid ? obj.uuid : null;
|
|
254
|
+
}
|
|
255
|
+
|
|
242
256
|
function mergeUsageEntry(ctx, entry) {
|
|
243
|
-
if (!entry.
|
|
257
|
+
if (!entry.dedupeKey) {
|
|
244
258
|
ctx.anonymousEntries.push(entry);
|
|
245
259
|
return;
|
|
246
260
|
}
|
|
247
|
-
const current = ctx.
|
|
248
|
-
// Claude sometimes copies the same
|
|
261
|
+
const current = ctx.entriesByKey.get(entry.dedupeKey);
|
|
262
|
+
// Claude sometimes copies the same record into another session with zeroed
|
|
249
263
|
// usage. Keep the most complete payload, independent of directory order.
|
|
250
264
|
if (!current || entry.usageScore > current.usageScore) {
|
|
251
|
-
ctx.
|
|
265
|
+
ctx.entriesByKey.set(entry.dedupeKey, entry);
|
|
252
266
|
}
|
|
253
267
|
}
|
|
254
268
|
|
|
255
269
|
export async function parse() {
|
|
256
270
|
const ctx = {
|
|
257
|
-
|
|
271
|
+
entriesByKey: new Map(),
|
|
258
272
|
anonymousEntries: [],
|
|
259
273
|
sessionEvents: [],
|
|
260
274
|
warnings: [],
|
|
@@ -283,8 +297,8 @@ export async function parse() {
|
|
|
283
297
|
|
|
284
298
|
const entries = [
|
|
285
299
|
...ctx.anonymousEntries,
|
|
286
|
-
...ctx.
|
|
287
|
-
].map(({
|
|
300
|
+
...ctx.entriesByKey.values(),
|
|
301
|
+
].map(({ dedupeKey: _dedupeKey, usageScore: _usageScore, ...entry }) => entry);
|
|
288
302
|
|
|
289
303
|
return {
|
|
290
304
|
buckets: aggregateToBuckets(entries),
|
package/src/parsers/dsh.js
CHANGED
|
@@ -190,33 +190,40 @@ function isUserMessageRecord(rec) {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
|
-
*
|
|
193
|
+
* Build a session model from one decompressed session log.
|
|
194
194
|
*
|
|
195
195
|
* Layout (DeepSeek Harness session-persistence-jsonl):
|
|
196
|
-
* line 0: {"type":"session","version":0,"id":...,"createdAt":...,"cwd"
|
|
196
|
+
* line 0: {"type":"session","version":0,"id":...,"createdAt":...,"cwd":...,
|
|
197
|
+
* "parentSession":...?, ...}
|
|
197
198
|
* ... possibly a resumed/forked seed replay, then ...
|
|
198
|
-
* {"type":"
|
|
199
|
-
* {"type":"assistant/message","data":{"turn","step","message":{"source":
|
|
200
|
-
* {"kind":"model","provider","model"},...},"usage":{"inputTokens",
|
|
201
|
-
* "outputTokens","cacheReadTokens","cacheWriteTokens",
|
|
202
|
-
* "reasoningTokens"}},...}
|
|
199
|
+
* {"type":"user/message"|"assistant/message","time":...,"data":{...}}
|
|
203
200
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
201
|
+
* DSH (developer preview) writes session/end-seed records in three situations:
|
|
202
|
+
* right after session creation (empty seed), at each resume boundary, and
|
|
203
|
+
* appended at the END of a file when that session becomes the seed for a
|
|
204
|
+
* further resume. The marker's position is therefore NOT a replay boundary —
|
|
205
|
+
* a trailing marker would make "skip everything before the last marker"
|
|
206
|
+
* discard the session's entire real history.
|
|
207
|
+
*
|
|
208
|
+
* Fork/subagent lineage is encoded separately in the immutable header.
|
|
209
|
+
* `parentSession` identifies the source and `seedLength` is the exact number
|
|
210
|
+
* of leading event seqs inherited from it. Only those seqs are skipped, and
|
|
211
|
+
* only while the parent file is also present, so a missing/corrupt source
|
|
212
|
+
* fails open instead of dropping the sole local copy of its usage.
|
|
213
|
+
*
|
|
214
|
+
* Only user/message (source.kind === 'user') and assistant/message records
|
|
215
|
+
* are kept in the model — they are the only records that produce usage
|
|
216
|
+
* entries or timing events. Their seq is retained so the header's seed
|
|
217
|
+
* boundary can be applied without inspecting or hashing message content.
|
|
208
218
|
*
|
|
209
219
|
* usage.outputTokens includes reasoningTokens (verified against the
|
|
210
220
|
* session_projcache totals DSH itself maintains), so reasoning is split out of
|
|
211
221
|
* output before aggregation, like the Pi-family parsers.
|
|
212
222
|
*/
|
|
213
|
-
function
|
|
214
|
-
const entries = [];
|
|
215
|
-
const events = [];
|
|
223
|
+
function buildSessionModel(text) {
|
|
216
224
|
const lines = text.split('\n');
|
|
217
225
|
|
|
218
226
|
let header = null;
|
|
219
|
-
let endSeedIndex = -1;
|
|
220
227
|
for (let i = 0; i < lines.length; i++) {
|
|
221
228
|
if (lines[i].length === 0) continue;
|
|
222
229
|
let rec;
|
|
@@ -225,9 +232,8 @@ function parseSessionText(text) {
|
|
|
225
232
|
} catch {
|
|
226
233
|
continue; // torn final line: keep the complete records
|
|
227
234
|
}
|
|
228
|
-
if (rec && typeof rec === 'object') {
|
|
229
|
-
|
|
230
|
-
if (rec.type === 'session/end-seed') endSeedIndex = i;
|
|
235
|
+
if (rec && typeof rec === 'object' && header === null && rec.type === 'session') {
|
|
236
|
+
header = rec;
|
|
231
237
|
}
|
|
232
238
|
}
|
|
233
239
|
|
|
@@ -243,11 +249,8 @@ function parseSessionText(text) {
|
|
|
243
249
|
throw error;
|
|
244
250
|
}
|
|
245
251
|
|
|
246
|
-
const
|
|
247
|
-
const project = projectFromCwd(header.cwd);
|
|
248
|
-
|
|
252
|
+
const messages = [];
|
|
249
253
|
for (let i = 0; i < lines.length; i++) {
|
|
250
|
-
if (i <= endSeedIndex) continue;
|
|
251
254
|
if (lines[i].length === 0) continue;
|
|
252
255
|
let rec;
|
|
253
256
|
try {
|
|
@@ -256,51 +259,142 @@ function parseSessionText(text) {
|
|
|
256
259
|
continue;
|
|
257
260
|
}
|
|
258
261
|
if (!rec || typeof rec !== 'object') continue;
|
|
259
|
-
const
|
|
260
|
-
if (
|
|
262
|
+
const timeMs = recordTimeMs(rec);
|
|
263
|
+
if (timeMs == null) continue;
|
|
264
|
+
const seq = Number.isSafeInteger(rec.seq) && rec.seq >= 0 ? rec.seq : null;
|
|
261
265
|
|
|
262
266
|
if (isUserMessageRecord(rec)) {
|
|
263
|
-
|
|
267
|
+
messages.push({ seq, role: 'user', timeMs, usage: null, model: null });
|
|
264
268
|
continue;
|
|
265
269
|
}
|
|
266
270
|
if (!isUsageRecord(rec)) continue;
|
|
267
271
|
|
|
268
272
|
// Every assistant/message marks the end of a billable step, even when its
|
|
269
|
-
// usage block is missing.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const reasoningOutputTokens = Math.min(
|
|
280
|
-
totalOutputTokens,
|
|
281
|
-
toCount(usage.reasoningTokens),
|
|
282
|
-
);
|
|
283
|
-
const outputTokens = totalOutputTokens - reasoningOutputTokens;
|
|
284
|
-
if (inputTokens + cachedInputTokens + reasoningOutputTokens + outputTokens === 0) continue;
|
|
285
|
-
|
|
286
|
-
const model =
|
|
287
|
-
typeof rec.data.message?.source?.model === 'string' && rec.data.message.source.model
|
|
288
|
-
? rec.data.message.source.model
|
|
289
|
-
: 'unknown';
|
|
290
|
-
|
|
291
|
-
entries.push({
|
|
292
|
-
source: SOURCE,
|
|
293
|
-
model,
|
|
294
|
-
project,
|
|
295
|
-
timestamp,
|
|
296
|
-
inputTokens,
|
|
297
|
-
outputTokens,
|
|
298
|
-
cachedInputTokens,
|
|
299
|
-
reasoningOutputTokens,
|
|
273
|
+
// usage block is missing; the model keeps it so timing survives.
|
|
274
|
+
messages.push({
|
|
275
|
+
seq,
|
|
276
|
+
role: 'assistant',
|
|
277
|
+
timeMs,
|
|
278
|
+
usage: parseUsage(rec.data.usage),
|
|
279
|
+
model:
|
|
280
|
+
typeof rec.data.message?.source?.model === 'string' && rec.data.message.source.model
|
|
281
|
+
? rec.data.message.source.model
|
|
282
|
+
: 'unknown',
|
|
300
283
|
});
|
|
301
284
|
}
|
|
302
285
|
|
|
303
|
-
return {
|
|
286
|
+
return {
|
|
287
|
+
sessionId: header.id,
|
|
288
|
+
parentSessionId:
|
|
289
|
+
typeof header.parentSession === 'string' && header.parentSession
|
|
290
|
+
? header.parentSession
|
|
291
|
+
: null,
|
|
292
|
+
seedLength:
|
|
293
|
+
Number.isSafeInteger(header.seedLength) && header.seedLength > 0
|
|
294
|
+
? header.seedLength
|
|
295
|
+
: 0,
|
|
296
|
+
cwd: header.cwd,
|
|
297
|
+
messages,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Record wall-clock time in epoch ms; null when absent/invalid. */
|
|
302
|
+
function recordTimeMs(rec) {
|
|
303
|
+
const t = rec.time;
|
|
304
|
+
if (typeof t === 'number' && Number.isFinite(t)) return t;
|
|
305
|
+
if (typeof t === 'string' && t.trim()) {
|
|
306
|
+
const d = new Date(t);
|
|
307
|
+
return Number.isNaN(d.getTime()) ? null : d.getTime();
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Usage numbers from an assistant/message usage block, or null when empty. */
|
|
313
|
+
function parseUsage(usage) {
|
|
314
|
+
if (!usage || typeof usage !== 'object') return null;
|
|
315
|
+
// Harness counts are disjoint. The common bucket model has no cache-write
|
|
316
|
+
// column, so cache writes join uncached input, matching the other parsers.
|
|
317
|
+
const inputTokens = toCount(usage.inputTokens) + toCount(usage.cacheWriteTokens);
|
|
318
|
+
const cachedInputTokens = toCount(usage.cacheReadTokens);
|
|
319
|
+
const totalOutputTokens = toCount(usage.outputTokens);
|
|
320
|
+
const reasoningOutputTokens = Math.min(totalOutputTokens, toCount(usage.reasoningTokens));
|
|
321
|
+
const outputTokens = totalOutputTokens - reasoningOutputTokens;
|
|
322
|
+
if (inputTokens + cachedInputTokens + reasoningOutputTokens + outputTokens === 0) return null;
|
|
323
|
+
return { inputTokens, outputTokens, cachedInputTokens, reasoningOutputTokens };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Token-accounting equality for a copied assistant record. */
|
|
327
|
+
function sameUsage(left, right) {
|
|
328
|
+
if (left == null || right == null) return left === right;
|
|
329
|
+
return (
|
|
330
|
+
left.inputTokens === right.inputTokens &&
|
|
331
|
+
left.outputTokens === right.outputTokens &&
|
|
332
|
+
left.cachedInputTokens === right.cachedInputTokens &&
|
|
333
|
+
left.reasoningOutputTokens === right.reasoningOutputTokens
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Number of leading child messages inherited from a parent seed.
|
|
339
|
+
*
|
|
340
|
+
* `header.seedLength` is DSH's durable fork-lineage boundary: event seqs below
|
|
341
|
+
* it came from the parent, while later seqs belong to the child. Each skipped
|
|
342
|
+
* message must still exist at the same seq in the selected parent copy.
|
|
343
|
+
* Missing, invalid, or divergent records fail open so usage is not lost.
|
|
344
|
+
*/
|
|
345
|
+
function replaySkipCount(child, parent) {
|
|
346
|
+
if (child.seedLength <= 0 || child.messages.length === 0) return 0;
|
|
347
|
+
let parentIndex = 0;
|
|
348
|
+
let previousSeq = -1;
|
|
349
|
+
let count = 0;
|
|
350
|
+
for (const message of child.messages) {
|
|
351
|
+
if (message.seq == null || message.seq <= previousSeq) return 0;
|
|
352
|
+
previousSeq = message.seq;
|
|
353
|
+
if (message.seq >= child.seedLength) break;
|
|
354
|
+
|
|
355
|
+
while (
|
|
356
|
+
parentIndex < parent.messages.length &&
|
|
357
|
+
parent.messages[parentIndex].seq != null &&
|
|
358
|
+
parent.messages[parentIndex].seq < message.seq
|
|
359
|
+
) {
|
|
360
|
+
parentIndex++;
|
|
361
|
+
}
|
|
362
|
+
const source = parent.messages[parentIndex];
|
|
363
|
+
if (
|
|
364
|
+
source?.seq !== message.seq ||
|
|
365
|
+
source.role !== message.role ||
|
|
366
|
+
source.model !== message.model ||
|
|
367
|
+
!sameUsage(source.usage, message.usage)
|
|
368
|
+
) {
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
parentIndex++;
|
|
372
|
+
count++;
|
|
373
|
+
}
|
|
374
|
+
return count;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Fold a (possibly replay-trimmed) model into flat usage entries + timing events. */
|
|
378
|
+
function modelToResult(model, skipCount) {
|
|
379
|
+
const sessionId = model.sessionId;
|
|
380
|
+
const project = projectFromCwd(model.cwd);
|
|
381
|
+
const entries = [];
|
|
382
|
+
const events = [];
|
|
383
|
+
for (let i = skipCount; i < model.messages.length; i++) {
|
|
384
|
+
const msg = model.messages[i];
|
|
385
|
+
const timestamp = new Date(msg.timeMs);
|
|
386
|
+
events.push({ sessionId, source: SOURCE, project, timestamp, role: msg.role });
|
|
387
|
+
if (msg.usage) {
|
|
388
|
+
entries.push({
|
|
389
|
+
source: SOURCE,
|
|
390
|
+
model: msg.model || 'unknown',
|
|
391
|
+
project,
|
|
392
|
+
timestamp,
|
|
393
|
+
...msg.usage,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return { entries, events };
|
|
304
398
|
}
|
|
305
399
|
|
|
306
400
|
/** List session log files under a DSH sessions root (session.jsonl[.zstd]). */
|
|
@@ -355,6 +449,12 @@ function listSessionFiles(sessionsDir, onFailure) {
|
|
|
355
449
|
* Zstandard session logs are multi-frame; node:zlib zstd (Node >= 22.15)
|
|
356
450
|
* decodes one frame per call, so the buffer is walked frame-by-frame, with a
|
|
357
451
|
* `zstd` CLI fallback for older Node.
|
|
452
|
+
*
|
|
453
|
+
* Replay handling: `header.parentSession` identifies a fork/subagent source,
|
|
454
|
+
* and `header.seedLength` is the exact count of leading event seqs inherited
|
|
455
|
+
* from it. Those records are skipped only when the parent file is also
|
|
456
|
+
* present. Files without either field, and children whose parent is missing,
|
|
457
|
+
* are counted in full. `session/end-seed` positions are never used.
|
|
358
458
|
*/
|
|
359
459
|
export async function parse() {
|
|
360
460
|
const sessionsDir = getDshSessionsDir();
|
|
@@ -385,7 +485,9 @@ export async function parse() {
|
|
|
385
485
|
return result;
|
|
386
486
|
}
|
|
387
487
|
|
|
388
|
-
|
|
488
|
+
// sessionId -> most complete model (largest decompressed log wins, so a
|
|
489
|
+
// session copied between project dirs is counted once).
|
|
490
|
+
const perSession = new Map();
|
|
389
491
|
for (const { file, compressed } of files) {
|
|
390
492
|
let text;
|
|
391
493
|
try {
|
|
@@ -406,9 +508,9 @@ export async function parse() {
|
|
|
406
508
|
continue;
|
|
407
509
|
}
|
|
408
510
|
|
|
409
|
-
let
|
|
511
|
+
let model;
|
|
410
512
|
try {
|
|
411
|
-
|
|
513
|
+
model = buildSessionModel(text);
|
|
412
514
|
} catch (error) {
|
|
413
515
|
recordFailure(
|
|
414
516
|
'dsh: skipping ' + relative(process.cwd(), file) + ' (' + error.message + ')',
|
|
@@ -417,17 +519,24 @@ export async function parse() {
|
|
|
417
519
|
}
|
|
418
520
|
|
|
419
521
|
const weight = text.length;
|
|
420
|
-
const previous = perSession.get(
|
|
522
|
+
const previous = perSession.get(model.sessionId);
|
|
421
523
|
if (!previous || weight > previous.weight) {
|
|
422
|
-
perSession.set(
|
|
524
|
+
perSession.set(model.sessionId, { model, weight });
|
|
423
525
|
}
|
|
424
526
|
}
|
|
425
527
|
|
|
426
528
|
const entries = [];
|
|
427
529
|
const eventsBySession = new Map();
|
|
428
|
-
for (const
|
|
429
|
-
|
|
430
|
-
|
|
530
|
+
for (const { model } of perSession.values()) {
|
|
531
|
+
// seedLength supplies the exact inherited boundary; matching source seqs
|
|
532
|
+
// prove the selected parent copy still contains what the child inherited.
|
|
533
|
+
// Missing/corrupt parents fail open so the child remains the local copy.
|
|
534
|
+
const parent =
|
|
535
|
+
model.parentSessionId == null ? null : perSession.get(model.parentSessionId);
|
|
536
|
+
const skip = parent ? replaySkipCount(model, parent.model) : 0;
|
|
537
|
+
const { entries: fileEntries, events: fileEvents } = modelToResult(model, skip);
|
|
538
|
+
entries.push(...fileEntries);
|
|
539
|
+
for (const event of fileEvents) {
|
|
431
540
|
if (!eventsBySession.has(event.sessionId)) eventsBySession.set(event.sessionId, []);
|
|
432
541
|
eventsBySession.get(event.sessionId).push(event);
|
|
433
542
|
}
|