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.
@@ -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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quilltap",
3
- "version": "4.10.0-dev.58",
3
+ "version": "4.10.0-dev.61",
4
4
  "description": "Self-hosted AI workspace for writers, worldbuilders, and roleplayers. Run with npx quilltap.",
5
5
  "author": {
6
6
  "name": "Charles Sebold",