quilltap 4.10.0-dev.58 → 4.10.0-dev.61
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/lib/db-commands.js +20 -0
- package/lib/db-helpers.js +12 -0
- package/lib/text-codec.js +61 -0
- package/package.json +1 -1
package/lib/db-commands.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
resolveProject,
|
|
17
17
|
} = require('./db-helpers');
|
|
18
18
|
const { getLockStatus } = require('./lock-helpers');
|
|
19
|
+
const { decodeText } = require('./text-codec');
|
|
19
20
|
|
|
20
21
|
// Tables grouped by domain for `db schema` (with-no-arg) overview, and for
|
|
21
22
|
// DB-routing when a verb names a specific table. Keep this list short — it's
|
|
@@ -477,6 +478,11 @@ function cmdMessages(args, ctx) {
|
|
|
477
478
|
params.push(last);
|
|
478
479
|
const rows = db.prepare(sql).all(...params).reverse(); // oldest first
|
|
479
480
|
|
|
481
|
+
// `content` may be a brotli BLOB (see lib/text-codec.js). Decode before
|
|
482
|
+
// anything prints or JSON-serializes it — a raw BLOB prints as binary and
|
|
483
|
+
// JSON.stringify turns it into a byte array.
|
|
484
|
+
for (const r of rows) r.content = decodeText(r.content);
|
|
485
|
+
|
|
480
486
|
if (json) return printJson({ chat, total: totalRow.n, returned: rows.length, messages: rows });
|
|
481
487
|
|
|
482
488
|
console.log(`Chat: ${chat.title} (${chat.id}) — showing ${rows.length} of ${totalRow.n} matching messages`);
|
|
@@ -557,6 +563,13 @@ function cmdMessage(args, ctx) {
|
|
|
557
563
|
try {
|
|
558
564
|
const row = db.prepare('SELECT * FROM chat_messages WHERE id = ?').get(id);
|
|
559
565
|
if (!row) throw new Error(`No chat_message with id ${id}`);
|
|
566
|
+
|
|
567
|
+
// The four large text columns may be brotli BLOBs (see lib/text-codec.js).
|
|
568
|
+
// Decode before anything prints or JSON-serializes them.
|
|
569
|
+
for (const col of ['content', 'opaqueContent', 'description', 'context']) {
|
|
570
|
+
if (col in row) row[col] = decodeText(row[col]);
|
|
571
|
+
}
|
|
572
|
+
|
|
560
573
|
if (json) return printJson(row);
|
|
561
574
|
|
|
562
575
|
printRecord(`Message ${row.id}`, {
|
|
@@ -596,6 +609,13 @@ function cmdLog(args, ctx) {
|
|
|
596
609
|
try {
|
|
597
610
|
const row = db.prepare('SELECT * FROM llm_logs WHERE id = ?').get(id);
|
|
598
611
|
if (!row) throw new Error(`No llm_log with id ${id}`);
|
|
612
|
+
|
|
613
|
+
// request/response may be brotli BLOBs (see lib/text-codec.js). Decode
|
|
614
|
+
// before anything reads, prints or JSON-serializes them — a raw BLOB
|
|
615
|
+
// would print as binary and JSON.stringify as a byte array.
|
|
616
|
+
row.request = decodeText(row.request);
|
|
617
|
+
row.response = decodeText(row.response);
|
|
618
|
+
|
|
599
619
|
if (json) return printJson(row);
|
|
600
620
|
|
|
601
621
|
let finishReason = null;
|
package/lib/db-helpers.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const fs = require('fs');
|
|
5
|
+
const { registerTextCodecFunction } = require('./text-codec');
|
|
5
6
|
|
|
6
7
|
const CTRL_C = String.fromCharCode(3);
|
|
7
8
|
const CTRL_D = String.fromCharCode(4);
|
|
@@ -201,6 +202,17 @@ function openEncryptedDb(dbPath, pepper, { readonly = true, friendlyName = 'data
|
|
|
201
202
|
'The database may be encrypted with a different key, or the .dbkey file may be missing.');
|
|
202
203
|
}
|
|
203
204
|
|
|
205
|
+
// Compressed text columns (llm_logs.request/response, chat_messages.content
|
|
206
|
+
// and friends) are BLOBs; qt_text() lets raw SQL and the repl read inside
|
|
207
|
+
// them. It is also REQUIRED for any --write that touches chat_messages: the
|
|
208
|
+
// message search triggers call it, so a connection without it fails the
|
|
209
|
+
// write loudly rather than letting the index drift.
|
|
210
|
+
try {
|
|
211
|
+
registerTextCodecFunction(db);
|
|
212
|
+
} catch {
|
|
213
|
+
// An old better-sqlite3 without db.function must not block a read.
|
|
214
|
+
}
|
|
215
|
+
|
|
204
216
|
return db;
|
|
205
217
|
}
|
|
206
218
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Text BLOB codec — the CLI's reader for compressed text columns.
|
|
5
|
+
*
|
|
6
|
+
* MIRROR OF `lib/database/text-compression.ts` in the server repo. The CLI is
|
|
7
|
+
* published independently and cannot import from the app, so the format is
|
|
8
|
+
* duplicated here deliberately. **If the header or codec changes there, change
|
|
9
|
+
* it here in the same commit** — a stale copy shows the user mojibake.
|
|
10
|
+
*
|
|
11
|
+
* Format (see the server module for the reasoning):
|
|
12
|
+
* [0] magic 0x51 ('Q') [1] version 0x01 [2] codec 0x01 (brotli) [3..] payload
|
|
13
|
+
*
|
|
14
|
+
* Values under 512 bytes are stored as plain TEXT, so a column holds a mix of
|
|
15
|
+
* compressed BLOBs and plain strings. `decodeText` accepts both, plus NULL and
|
|
16
|
+
* uncompressed buffers, and never throws.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const zlib = require('zlib');
|
|
20
|
+
|
|
21
|
+
const TEXT_BLOB_MAGIC = 0x51;
|
|
22
|
+
const TEXT_BLOB_VERSION = 0x01;
|
|
23
|
+
const TEXT_CODEC_BROTLI = 0x01;
|
|
24
|
+
const TEXT_BLOB_HEADER_BYTES = 3;
|
|
25
|
+
|
|
26
|
+
/** Does this value carry the compressed-text header? */
|
|
27
|
+
function isCompressedTextBlob(value) {
|
|
28
|
+
if (!Buffer.isBuffer(value) || value.length < TEXT_BLOB_HEADER_BYTES) return false;
|
|
29
|
+
return (
|
|
30
|
+
value[0] === TEXT_BLOB_MAGIC &&
|
|
31
|
+
value[1] === TEXT_BLOB_VERSION &&
|
|
32
|
+
value[2] === TEXT_CODEC_BROTLI
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Decode a stored value to text. Total: never throws, never returns undefined. */
|
|
37
|
+
function decodeText(value) {
|
|
38
|
+
if (value === null || value === undefined) return null;
|
|
39
|
+
if (typeof value === 'string') return value;
|
|
40
|
+
if (!Buffer.isBuffer(value)) return String(value);
|
|
41
|
+
if (!isCompressedTextBlob(value)) return value.toString('utf-8');
|
|
42
|
+
|
|
43
|
+
const payload = value.subarray(TEXT_BLOB_HEADER_BYTES);
|
|
44
|
+
try {
|
|
45
|
+
return zlib.brotliDecompressSync(payload).toString('utf-8');
|
|
46
|
+
} catch {
|
|
47
|
+
return payload.toString('utf-8');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Register `qt_text()` on a connection, so raw SQL (`quilltap db "SELECT …"`,
|
|
53
|
+
* `--repl`) can read inside a compressed column:
|
|
54
|
+
*
|
|
55
|
+
* SELECT json_extract(qt_text(response), '$.error') FROM llm_logs;
|
|
56
|
+
*/
|
|
57
|
+
function registerTextCodecFunction(db) {
|
|
58
|
+
db.function('qt_text', { deterministic: true }, (value) => decodeText(value));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { decodeText, isCompressedTextBlob, registerTextCodecFunction };
|