quilltap 4.10.0-dev.57 → 4.10.0-dev.59
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 +8 -0
- package/lib/db-helpers.js +9 -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
|
|
@@ -596,6 +597,13 @@ function cmdLog(args, ctx) {
|
|
|
596
597
|
try {
|
|
597
598
|
const row = db.prepare('SELECT * FROM llm_logs WHERE id = ?').get(id);
|
|
598
599
|
if (!row) throw new Error(`No llm_log with id ${id}`);
|
|
600
|
+
|
|
601
|
+
// request/response may be brotli BLOBs (see lib/text-codec.js). Decode
|
|
602
|
+
// before anything reads, prints or JSON-serializes them — a raw BLOB
|
|
603
|
+
// would print as binary and JSON.stringify as a byte array.
|
|
604
|
+
row.request = decodeText(row.request);
|
|
605
|
+
row.response = decodeText(row.response);
|
|
606
|
+
|
|
599
607
|
if (json) return printJson(row);
|
|
600
608
|
|
|
601
609
|
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,14 @@ 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 and friends) are
|
|
206
|
+
// BLOBs; qt_text() lets raw SQL and the repl read inside them.
|
|
207
|
+
try {
|
|
208
|
+
registerTextCodecFunction(db);
|
|
209
|
+
} catch {
|
|
210
|
+
// An old better-sqlite3 without db.function must not block a read.
|
|
211
|
+
}
|
|
212
|
+
|
|
204
213
|
return db;
|
|
205
214
|
}
|
|
206
215
|
|
|
@@ -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 };
|