quilltap 4.7.0-dev → 4.7.0-dev.117
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 +38 -4
- package/bin/quilltap.js +63 -5
- package/lib/__tests__/qtap-uri.test.js +111 -0
- package/lib/completion/bash.template +1 -1
- package/lib/completion/fish.template +2 -1
- package/lib/completion/zsh.template +2 -1
- package/lib/docs-commands.js +423 -19
- package/lib/lock-helpers.js +242 -0
- package/lib/maintenance-commands.js +394 -0
- package/lib/native-modules.js +91 -32
- package/lib/qtap-uri.js +171 -0
- package/lib/theme-validation.js +37 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -141,12 +141,16 @@ Most subcommands accept `--json` (for piping) and `--limit N`. Names are case-in
|
|
|
141
141
|
```bash
|
|
142
142
|
quilltap db --tables # List tables in active DB
|
|
143
143
|
quilltap db --count chat_messages # Row count
|
|
144
|
-
quilltap db "SELECT id FROM characters LIMIT 5" # Raw SQL
|
|
145
|
-
quilltap db --repl # Interactive prompt
|
|
144
|
+
quilltap db "SELECT id FROM characters LIMIT 5" # Raw SQL (read-only)
|
|
145
|
+
quilltap db --repl # Interactive prompt (read-only)
|
|
146
|
+
quilltap db --write "UPDATE characters SET title = 'rival' WHERE id = '...'"
|
|
147
|
+
quilltap db --repl --write # Interactive, read-write
|
|
146
148
|
quilltap db --llm-logs --tables # Target the LLM logs DB
|
|
147
149
|
quilltap db --mount-points --tables # Target the mount index DB
|
|
148
150
|
```
|
|
149
151
|
|
|
152
|
+
The database is opened **read-only by default**. Add `--write` to make changes: it opens the database read-write, **claims the instance lock** (`<dataDir>/quilltap.lock`) for the duration, and releases it on exit. It **refuses — with no override — if a running server or another instance holds the lock**, so stop the server first. `--repl` is read-only unless combined with `--write`. Attempting a write without `--write` fails with a hint to re-run with the flag.
|
|
153
|
+
|
|
150
154
|
In the REPL, `.cols <table>` and `.find <text>` mirror the subcommand helpers.
|
|
151
155
|
|
|
152
156
|
## Document Stores (Scriptorium)
|
|
@@ -169,14 +173,32 @@ quilltap docs status # Per-mount extraction + embeddi
|
|
|
169
173
|
quilltap docs scan <mount> # Trigger a rescan
|
|
170
174
|
quilltap docs reindex <mount> [path] [--force] # Re-extract + re-chunk
|
|
171
175
|
quilltap docs embed <mount> [path] [--force] [--wait] # Enqueue embedding jobs
|
|
172
|
-
quilltap docs write [--force] <mount> <path> [file]
|
|
176
|
+
quilltap docs write [--force] [--base64] <mount> <path> [file] # Stdin or file → mount
|
|
177
|
+
quilltap docs read [--rendered] [--base64] <mount> <path> # File contents → stdout
|
|
173
178
|
quilltap docs delete <mount> <path> # Idempotent delete
|
|
174
179
|
quilltap docs mkdir <mount> <path> # Idempotent folder create
|
|
175
180
|
quilltap docs move <srcMount> <srcPath> <dstMount> <dstPath> # Move (hard-link when possible)
|
|
176
181
|
quilltap docs copy [--force] <srcMount> <srcPath> <dstMount> <dstPath>
|
|
182
|
+
quilltap docs link <srcMount> <srcPath> <dstMount> <dstPath> # Hard-link (server-required; no byte copy)
|
|
183
|
+
quilltap docs rmdir <mount> <path> # Delete an empty folder (server-required)
|
|
184
|
+
quilltap docs mvdir <mount> <fromPath> <toPath> # Rename/move a folder (server-required)
|
|
177
185
|
```
|
|
178
186
|
|
|
179
|
-
Mount arguments accept the mount name (case-insensitive) or a UUID; ambiguous names print candidates and exit non-zero. `--json` is supported by every verb; `reindex` and `
|
|
187
|
+
Mount arguments accept the mount name (case-insensitive) or a UUID; ambiguous names print candidates and exit non-zero. `--json` is supported by every verb; `reindex`, `embed`, `link`, `rmdir`, and `mvdir` refuse to run without a reachable server.
|
|
188
|
+
|
|
189
|
+
### `--base64` flag
|
|
190
|
+
|
|
191
|
+
`write --base64`: reads the source bytes and sends them to the server via `PUT /api/v1/mount-points/{mountId}/files/{path}` with `{content: <base64>, encoding: "base64"}`. This is the portable path the file browser uses and handles arbitrary binary files cleanly. Server-required; the direct filesystem fallback is not available with this flag.
|
|
192
|
+
|
|
193
|
+
`read --base64`: fetches raw bytes via `GET /api/v1/mount-points/{mountId}/files/{path}?encoding=base64` and emits the decoded bytes to stdout, suitable for binary round-trips. Server-required.
|
|
194
|
+
|
|
195
|
+
### `link`, `rmdir`, `mvdir`
|
|
196
|
+
|
|
197
|
+
`link` calls `POST /api/v1/mount-points/{srcMountId}?action=link-file` with `{sourcePath, destMountPointId, destPath}`. Creates a true hard link with no byte copy; the server reports back a `strategy` field. Errors: `DEST_EXISTS` (exit 2), `UNSUPPORTED` (cross-storage or cross-device), `SOURCE_NOT_FOUND`.
|
|
198
|
+
|
|
199
|
+
`rmdir` calls `POST /api/v1/mount-points/{mountId}?action=delete-folder` with `{path}`. Fails with a clear message if the folder is not empty (`NOT_EMPTY` / `CONFLICT`).
|
|
200
|
+
|
|
201
|
+
`mvdir` calls `POST /api/v1/mount-points/{mountId}?action=move-folder` with `{fromPath, toPath}`. Fails with exit 2 if the destination already exists (`DEST_EXISTS`).
|
|
180
202
|
|
|
181
203
|
## Memories
|
|
182
204
|
|
|
@@ -194,6 +216,18 @@ quilltap memories status [--character <name|id>] # Per-hol
|
|
|
194
216
|
|
|
195
217
|
Shared filter flags apply to `ls`, `find`, `grep`, and `status` where they make sense: `--character`, `--about` (with `self` / `none` shortcuts), `--source`, `--chat` (with `none` for manual entries), `--project`, `--since`, `--until`, `--min-importance`, `--min-reinforced`, `--has-embedding` / `--no-embedding`. Sort flags (`--sort reinforced|importance|created|accessed|reinforcement-count|links`, plus `-r` to reverse) apply to `ls`, `find`, and `grep`. Names accept fuzzy substrings; ambiguous names print candidates and exit 2. `--json` is supported by every verb. The legacy `quilltap db memories --character <name>` verb remains undisturbed.
|
|
196
218
|
|
|
219
|
+
## Maintenance & Cleanup
|
|
220
|
+
|
|
221
|
+
`quilltap maintenance` is the manual trigger for the retention sweeps that otherwise run on the server's daily maintenance tick. It reaps data with no bearing on characters, stories, or memories.
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
quilltap maintenance status # Read-only: last sweep time + dry-run counts of what would be reaped
|
|
225
|
+
quilltap maintenance status --instance Friday --json
|
|
226
|
+
quilltap maintenance run --instance Friday # Run the sweeps once (lock-gated; refuses while the server is up)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`maintenance run` is a DB writer: it claims `<dataDir>/quilltap.lock` and **refuses while a running Quilltap server holds it** — stop the server first. Because it can only run with the server down, it performs the sweeps expressible as direct SQL/filesystem work: reaping finished background jobs (COMPLETED after 7 days, DEAD after 30, keyed off `completedAt`), closed terminal sessions older than 30 days plus their transcript files, and orphaned mount-index files. The **stale-chat asset collapse** (superseded story-backgrounds and wardrobe avatars) needs the server's file-storage machinery and runs only on the server's daily tick — `status` reports a stale-chat count so you can see the backlog. Retention windows mirror `lib/background-jobs/maintenance/retention-constants.ts`.
|
|
230
|
+
|
|
197
231
|
## Theme Management
|
|
198
232
|
|
|
199
233
|
The CLI includes theme management commands:
|
package/bin/quilltap.js
CHANGED
|
@@ -13,7 +13,7 @@ const {
|
|
|
13
13
|
loadDbKey,
|
|
14
14
|
} = require('../lib/db-helpers');
|
|
15
15
|
const { resolveInstance } = require('../lib/instances');
|
|
16
|
-
const { resolveModuleDir, ensureNativeModules } = require('../lib/native-modules');
|
|
16
|
+
const { resolveModuleDir, ensureNativeModules, ensureDatabaseNativeModule } = require('../lib/native-modules');
|
|
17
17
|
|
|
18
18
|
const PACKAGE_DIR = path.resolve(__dirname, '..');
|
|
19
19
|
|
|
@@ -730,9 +730,15 @@ Subcommands (high-level shortcuts; auto-pick the right database):
|
|
|
730
730
|
Most subcommands also accept --json and --limit N.
|
|
731
731
|
|
|
732
732
|
Low-level options (legacy; still supported):
|
|
733
|
+
The database is opened READ-ONLY by default. Add --write to make changes.
|
|
733
734
|
--tables List all tables in the active database
|
|
734
735
|
--count <table> Show row count for a table
|
|
735
|
-
--repl Interactive SQL prompt (extras: .cols, .find)
|
|
736
|
+
--repl Interactive SQL prompt (extras: .cols, .find).
|
|
737
|
+
Read-only unless combined with --write.
|
|
738
|
+
--write Open the database read-write. Claims the instance lock
|
|
739
|
+
for the duration and releases it on exit. Refuses (no
|
|
740
|
+
override) if a running server or another instance holds
|
|
741
|
+
the lock — stop it first. Works with raw SQL and --repl.
|
|
736
742
|
--json Emit machine-readable JSON instead of a table
|
|
737
743
|
(works with --tables, --count, and raw SQL)
|
|
738
744
|
--llm-logs Target the LLM logs database
|
|
@@ -769,6 +775,8 @@ Examples:
|
|
|
769
775
|
quilltap db "SELECT count(*) FROM characters"
|
|
770
776
|
quilltap db --count messages
|
|
771
777
|
quilltap db --repl
|
|
778
|
+
quilltap db --write "UPDATE characters SET title = 'rival' WHERE id = '...'"
|
|
779
|
+
quilltap db --repl --write # interactive, read-write
|
|
772
780
|
quilltap db --lock-status
|
|
773
781
|
QUILLTAP_DB_PASSPHRASE=secret quilltap db --tables
|
|
774
782
|
`);
|
|
@@ -837,6 +845,7 @@ async function dbCommand(args) {
|
|
|
837
845
|
let showTables = false;
|
|
838
846
|
let countTable = '';
|
|
839
847
|
let repl = false;
|
|
848
|
+
let writable = false;
|
|
840
849
|
let sql = '';
|
|
841
850
|
let showHelp = false;
|
|
842
851
|
let lockStatus = false;
|
|
@@ -852,6 +861,7 @@ async function dbCommand(args) {
|
|
|
852
861
|
case '--tables': showTables = true; break;
|
|
853
862
|
case '--count': countTable = cleaned[++i]; break;
|
|
854
863
|
case '--repl': repl = true; break;
|
|
864
|
+
case '--write': writable = true; break;
|
|
855
865
|
case '--json': asJson = true; break;
|
|
856
866
|
case '--help': case '-h': showHelp = true; break;
|
|
857
867
|
case '--lock-status': lockStatus = true; break;
|
|
@@ -919,6 +929,19 @@ async function dbCommand(args) {
|
|
|
919
929
|
process.exit(1);
|
|
920
930
|
}
|
|
921
931
|
|
|
932
|
+
// Read-write opens (`--write`, optionally with `--repl`) must claim the
|
|
933
|
+
// instance lock first so a server starting mid-operation refuses to run.
|
|
934
|
+
// Refuses (no override) if a live instance already holds it.
|
|
935
|
+
if (writable) {
|
|
936
|
+
const { acquireWriteLock } = require('../lib/lock-helpers');
|
|
937
|
+
try {
|
|
938
|
+
acquireWriteLock(dataDir);
|
|
939
|
+
} catch (err) {
|
|
940
|
+
console.error(err.message);
|
|
941
|
+
process.exit(1);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
922
945
|
// Open database — prefer SQLCipher-capable build
|
|
923
946
|
let Database;
|
|
924
947
|
try {
|
|
@@ -926,7 +949,7 @@ async function dbCommand(args) {
|
|
|
926
949
|
} catch {
|
|
927
950
|
Database = require('better-sqlite3');
|
|
928
951
|
}
|
|
929
|
-
const db = new Database(dbPath, { readonly: !
|
|
952
|
+
const db = new Database(dbPath, { readonly: !writable });
|
|
930
953
|
|
|
931
954
|
if (pepper) {
|
|
932
955
|
const keyHex = Buffer.from(pepper, 'base64').toString('hex');
|
|
@@ -971,7 +994,7 @@ async function dbCommand(args) {
|
|
|
971
994
|
} else if (repl) {
|
|
972
995
|
const readline = require('readline');
|
|
973
996
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: 'quilltap> ' });
|
|
974
|
-
console.log(`Connected to ${dbPath}`);
|
|
997
|
+
console.log(`Connected to ${dbPath}${writable ? ' (read-write — instance lock held)' : ' (read-only)'}`);
|
|
975
998
|
console.log('Type .tables, .schema <table>, .cols <table>, .find <text>, or SQL. Ctrl+D to exit.\n');
|
|
976
999
|
rl.prompt();
|
|
977
1000
|
rl.on('line', (line) => {
|
|
@@ -1033,19 +1056,40 @@ async function dbCommand(args) {
|
|
|
1033
1056
|
}
|
|
1034
1057
|
} catch (err) {
|
|
1035
1058
|
console.error(`Error: ${err.message}`);
|
|
1059
|
+
if (!writable && /readonly|read-only/i.test(err.message)) {
|
|
1060
|
+
console.error('This REPL is read-only. Exit and re-run as `quilltap db --repl --write` to make changes.');
|
|
1061
|
+
}
|
|
1036
1062
|
}
|
|
1037
1063
|
rl.prompt();
|
|
1038
1064
|
});
|
|
1039
1065
|
rl.on('close', () => {
|
|
1040
1066
|
db.close();
|
|
1067
|
+
if (writable) {
|
|
1068
|
+
const { releaseWriteLock } = require('../lib/lock-helpers');
|
|
1069
|
+
releaseWriteLock(dataDir);
|
|
1070
|
+
}
|
|
1041
1071
|
process.exit(0);
|
|
1042
1072
|
});
|
|
1043
1073
|
return; // Don't close db yet — REPL is interactive
|
|
1044
1074
|
} else {
|
|
1045
1075
|
printDbHelp();
|
|
1046
1076
|
}
|
|
1077
|
+
} catch (err) {
|
|
1078
|
+
if (!writable && /readonly|read-only/i.test(err.message)) {
|
|
1079
|
+
console.error('This database was opened read-only, so it cannot be modified.');
|
|
1080
|
+
console.error('Re-run with --write to make changes — it claims the instance lock while it runs');
|
|
1081
|
+
console.error('and refuses if another Quilltap instance is using the database. For example:');
|
|
1082
|
+
console.error(` quilltap db --write ${sql ? JSON.stringify(sql) : '"UPDATE ..."'}`);
|
|
1083
|
+
} else {
|
|
1084
|
+
console.error(`Error: ${err.message}`);
|
|
1085
|
+
}
|
|
1086
|
+
process.exitCode = 1;
|
|
1047
1087
|
} finally {
|
|
1048
1088
|
if (!repl) db.close();
|
|
1089
|
+
if (writable && !repl) {
|
|
1090
|
+
const { releaseWriteLock } = require('../lib/lock-helpers');
|
|
1091
|
+
releaseWriteLock(dataDir);
|
|
1092
|
+
}
|
|
1049
1093
|
}
|
|
1050
1094
|
}
|
|
1051
1095
|
|
|
@@ -1059,7 +1103,7 @@ async function dbCommand(args) {
|
|
|
1059
1103
|
// to the subcommand. Each subcommand parses these flags position-independently,
|
|
1060
1104
|
// so they behave the same before or after the verb.
|
|
1061
1105
|
const SUBCOMMANDS = new Set([
|
|
1062
|
-
'db', 'themes', 'docs', 'memories', 'instances', 'memory-diff', 'completion', 'logs', 'migrations',
|
|
1106
|
+
'db', 'themes', 'docs', 'memories', 'instances', 'memory-diff', 'completion', 'logs', 'migrations', 'maintenance',
|
|
1063
1107
|
]);
|
|
1064
1108
|
// Global flags that consume the following token as their value.
|
|
1065
1109
|
const GLOBAL_VALUE_FLAGS = new Set(['-p', '--port', '-d', '--data-dir', '-i', '--instance', '--passphrase']);
|
|
@@ -1080,6 +1124,14 @@ const subName = subIdx >= 0 ? cliArgs[subIdx] : '';
|
|
|
1080
1124
|
// Everything except the subcommand token itself (leading global flags kept).
|
|
1081
1125
|
const subArgs = subIdx >= 0 ? [...cliArgs.slice(0, subIdx), ...cliArgs.slice(subIdx + 1)] : [];
|
|
1082
1126
|
|
|
1127
|
+
// Subcommands load the SQLCipher binding directly and never reach main()'s
|
|
1128
|
+
// native-module heal, so self-heal the database ABI here first. Cheap no-op when
|
|
1129
|
+
// healthy; rebuilds (with a friendly notice, not an error) only on a real
|
|
1130
|
+
// Node-ABI mismatch — e.g. after the user upgrades Node under a cached install.
|
|
1131
|
+
if (subName) {
|
|
1132
|
+
ensureDatabaseNativeModule();
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1083
1135
|
if (subName === 'db') {
|
|
1084
1136
|
dbCommand(subArgs);
|
|
1085
1137
|
} else if (subName === 'themes') {
|
|
@@ -1127,6 +1179,12 @@ if (subName === 'db') {
|
|
|
1127
1179
|
console.error(`Error: ${err.message}`);
|
|
1128
1180
|
process.exit(1);
|
|
1129
1181
|
});
|
|
1182
|
+
} else if (subName === 'maintenance') {
|
|
1183
|
+
const { maintenanceCommand } = require('../lib/maintenance-commands');
|
|
1184
|
+
maintenanceCommand(subArgs).catch(err => {
|
|
1185
|
+
console.error(`Error: ${err.message}`);
|
|
1186
|
+
process.exit(1);
|
|
1187
|
+
});
|
|
1130
1188
|
} else {
|
|
1131
1189
|
main();
|
|
1132
1190
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the CLI-local qtap:// codec (packages/quilltap/lib/qtap-uri.js).
|
|
3
|
+
* Mirrors the server codec's tests (§3.4 / §2.3) — same grammar, same encoding.
|
|
4
|
+
*
|
|
5
|
+
* @jest-environment node
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
isQtapUri,
|
|
12
|
+
parseQtapUri,
|
|
13
|
+
formatQtapUri,
|
|
14
|
+
formatDocStoreUri,
|
|
15
|
+
QtapUriError,
|
|
16
|
+
} = require('../qtap-uri');
|
|
17
|
+
|
|
18
|
+
describe('CLI isQtapUri', () => {
|
|
19
|
+
it('accepts qtap:// (case-insensitive), rejects others', () => {
|
|
20
|
+
expect(isQtapUri('qtap://self/x.md')).toBe(true);
|
|
21
|
+
expect(isQtapUri('QTAP://self/x.md')).toBe(true);
|
|
22
|
+
expect(isQtapUri('https://x')).toBe(false);
|
|
23
|
+
expect(isQtapUri('self/x')).toBe(false);
|
|
24
|
+
expect(isQtapUri(undefined)).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('CLI parseQtapUri — worked examples', () => {
|
|
29
|
+
it('self vault', () => {
|
|
30
|
+
expect(parseQtapUri('qtap://self/Mail/a.md')).toEqual({
|
|
31
|
+
scope: 'document_store',
|
|
32
|
+
mountPoint: 'self',
|
|
33
|
+
path: 'Mail/a.md',
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('encoded name with space + colon', () => {
|
|
38
|
+
const p = parseQtapUri('qtap://Project%20Files%3A%20Voyages/Knowledge/x.md');
|
|
39
|
+
expect(p.mountPoint).toBe('Project Files: Voyages');
|
|
40
|
+
expect(p.path).toBe('Knowledge/x.md');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('UUID authority', () => {
|
|
44
|
+
expect(parseQtapUri('qtap://550e8400-e29b-41d4-a716-446655440000/n/t.md').mountPoint).toBe(
|
|
45
|
+
'550e8400-e29b-41d4-a716-446655440000'
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('project + general scopes', () => {
|
|
50
|
+
expect(parseQtapUri('qtap://project/Outline.md')).toEqual({ scope: 'project', path: 'Outline.md' });
|
|
51
|
+
expect(parseQtapUri('qtap://general/S/intro.md')).toEqual({ scope: 'general', path: 'S/intro.md' });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('fragment heading + level', () => {
|
|
55
|
+
expect(parseQtapUri('qtap://self/Backstory.md#Childhood:2')).toEqual({
|
|
56
|
+
scope: 'document_store',
|
|
57
|
+
mountPoint: 'self',
|
|
58
|
+
path: 'Backstory.md',
|
|
59
|
+
heading: 'Childhood',
|
|
60
|
+
level: 2,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('store root, with and without trailing slash', () => {
|
|
65
|
+
expect(parseQtapUri('qtap://self/').path).toBe('');
|
|
66
|
+
expect(parseQtapUri('qtap://self').path).toBe('');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('reserved words win, case-insensitively', () => {
|
|
70
|
+
expect(parseQtapUri('qtap://SELF/x').mountPoint).toBe('self');
|
|
71
|
+
expect(parseQtapUri('qtap://Project/x').scope).toBe('project');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('CLI parseQtapUri — errors', () => {
|
|
76
|
+
it('NOT_A_QTAP_URI / EMPTY_AUTHORITY / BAD_LEVEL', () => {
|
|
77
|
+
expect(() => parseQtapUri('https://x')).toThrow(QtapUriError);
|
|
78
|
+
expect(() => parseQtapUri('qtap:///foo')).toThrow(/empty authority/i);
|
|
79
|
+
expect(() => parseQtapUri('qtap://self/x#H:0')).toThrow(/level/i);
|
|
80
|
+
expect(() => parseQtapUri('qtap://self/x#H:x')).toThrow(/level/i);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('CLI formatQtapUri — canonical', () => {
|
|
85
|
+
it('encodes colon as %3A and spaces as %20', () => {
|
|
86
|
+
expect(
|
|
87
|
+
formatQtapUri({ scope: 'document_store', mountPoint: 'A: B', path: 'c d.md' })
|
|
88
|
+
).toBe('qtap://A%3A%20B/c%20d.md');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('store root has trailing slash', () => {
|
|
92
|
+
expect(formatQtapUri({ scope: 'document_store', mountPoint: 'self', path: '' })).toBe('qtap://self/');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('formatDocStoreUri builds a store URI', () => {
|
|
96
|
+
expect(formatDocStoreUri('My Store', 'a/b.md')).toBe('qtap://My%20Store/a/b.md');
|
|
97
|
+
expect(formatDocStoreUri('My Store', '')).toBe('qtap://My%20Store/');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('CLI round-trip', () => {
|
|
102
|
+
const cases = [
|
|
103
|
+
['qtap://self/Mail/a.md', 'qtap://self/Mail/a.md'],
|
|
104
|
+
['qtap://Project%20Files:%20Voyages/k.md', 'qtap://Project%20Files%3A%20Voyages/k.md'],
|
|
105
|
+
['qtap://project/Outline.md', 'qtap://project/Outline.md'],
|
|
106
|
+
['qtap://self', 'qtap://self/'],
|
|
107
|
+
];
|
|
108
|
+
it.each(cases)('%s → %s', (input, expected) => {
|
|
109
|
+
expect(formatQtapUri(parseQtapUri(input))).toBe(expected);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -113,7 +113,7 @@ _quilltap_complete() {
|
|
|
113
113
|
local db_flags="--instance --data-dir --passphrase --json --limit --grep \
|
|
114
114
|
--character --project --about --source --chat --message --rendered --field \
|
|
115
115
|
--tail --last --full --from --type --out --id --diverged --blocked --help \
|
|
116
|
-
--tables --count --repl --llm-logs --mount-points \
|
|
116
|
+
--tables --count --repl --write --llm-logs --mount-points \
|
|
117
117
|
--lock-status --lock-clean --lock-override"
|
|
118
118
|
if [[ -z "$subverb" ]]; then
|
|
119
119
|
if [[ "$cur" == -* ]]; then
|
|
@@ -109,7 +109,8 @@ complete -c quilltap -n '__quilltap_using_subcommand db' -l 'type' -d 'Filter by
|
|
|
109
109
|
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'out' -d 'Output directory' -r -F
|
|
110
110
|
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'tables' -d 'List tables (low-level)'
|
|
111
111
|
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'count' -d 'Count rows in table' -x
|
|
112
|
-
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'repl' -d 'Open SQL REPL'
|
|
112
|
+
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'repl' -d 'Open SQL REPL (read-only unless --write)'
|
|
113
|
+
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'write' -d 'Open database read-write (lock-gated)'
|
|
113
114
|
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'llm-logs' -d 'Target llm-logs database'
|
|
114
115
|
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'mount-points' -d 'Target mount-index database'
|
|
115
116
|
complete -c quilltap -n '__quilltap_using_subcommand db' -l 'lock-status' -d 'Show instance lock status'
|
|
@@ -125,7 +125,8 @@ _quilltap_db() {
|
|
|
125
125
|
'--blocked[Only characters with vault issues]'
|
|
126
126
|
'--tables[List tables (low-level)]'
|
|
127
127
|
'--count[Count rows in table]:table:'
|
|
128
|
-
'--repl[Open SQL REPL]'
|
|
128
|
+
'--repl[Open SQL REPL (read-only unless --write)]'
|
|
129
|
+
'--write[Open database read-write (lock-gated; refuses if an instance holds the lock)]'
|
|
129
130
|
'--llm-logs[Target llm-logs database]'
|
|
130
131
|
'--mount-points[Target mount-index database]'
|
|
131
132
|
'--lock-status[Show instance lock status]'
|