quilltap 4.6.0-dev.22 → 4.6.0-dev.41
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/bin/quilltap.js +18 -4
- package/lib/db-commands.js +325 -0
- package/package.json +1 -1
package/bin/quilltap.js
CHANGED
|
@@ -782,6 +782,11 @@ Subcommands (high-level shortcuts; auto-pick the right database):
|
|
|
782
782
|
log <id> Full request/response of a single LLM log
|
|
783
783
|
memories --character <id> Memories held by a character
|
|
784
784
|
(flags: --about <id|name> --source AUTO|MANUAL)
|
|
785
|
+
characters status Per-character vault readiness report:
|
|
786
|
+
flag value, vault present, single-file count,
|
|
787
|
+
Prompts/ and Scenarios/ folder counts, and any
|
|
788
|
+
divergence between DB columns and vault content.
|
|
789
|
+
(flags: --id <id|name> --diverged --blocked --limit N)
|
|
785
790
|
optimize [target...] Run maintenance (VACUUM + ANALYZE + PRAGMA optimize)
|
|
786
791
|
on the named databases, or all of them if no
|
|
787
792
|
target is given. Targets: main, llm-logs,
|
|
@@ -804,6 +809,8 @@ Low-level options (legacy; still supported):
|
|
|
804
809
|
--tables List all tables in the active database
|
|
805
810
|
--count <table> Show row count for a table
|
|
806
811
|
--repl Interactive SQL prompt (extras: .cols, .find)
|
|
812
|
+
--json Emit machine-readable JSON instead of a table
|
|
813
|
+
(works with --tables, --count, and raw SQL)
|
|
807
814
|
--llm-logs Target the LLM logs database
|
|
808
815
|
--mount-points Target the document mount-index database
|
|
809
816
|
--data-dir <path> Override data directory (pass instance root)
|
|
@@ -911,6 +918,7 @@ async function dbCommand(args) {
|
|
|
911
918
|
let lockStatus = false;
|
|
912
919
|
let lockClean = false;
|
|
913
920
|
let lockOverride = false;
|
|
921
|
+
let asJson = false;
|
|
914
922
|
|
|
915
923
|
let i = 0;
|
|
916
924
|
while (i < cleaned.length) {
|
|
@@ -920,6 +928,7 @@ async function dbCommand(args) {
|
|
|
920
928
|
case '--tables': showTables = true; break;
|
|
921
929
|
case '--count': countTable = cleaned[++i]; break;
|
|
922
930
|
case '--repl': repl = true; break;
|
|
931
|
+
case '--json': asJson = true; break;
|
|
923
932
|
case '--help': case '-h': showHelp = true; break;
|
|
924
933
|
case '--lock-status': lockStatus = true; break;
|
|
925
934
|
case '--lock-clean': lockClean = true; break;
|
|
@@ -1013,22 +1022,27 @@ async function dbCommand(args) {
|
|
|
1013
1022
|
try {
|
|
1014
1023
|
if (showTables) {
|
|
1015
1024
|
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all();
|
|
1016
|
-
|
|
1025
|
+
if (asJson) console.log(JSON.stringify(tables.map(t => t.name), null, 2));
|
|
1026
|
+
else for (const t of tables) console.log(t.name);
|
|
1017
1027
|
} else if (countTable) {
|
|
1018
1028
|
const row = db.prepare(`SELECT count(*) as count FROM "${countTable}"`).get();
|
|
1019
|
-
console.log(row.count);
|
|
1029
|
+
if (asJson) console.log(JSON.stringify({ table: countTable, count: row.count }, null, 2));
|
|
1030
|
+
else console.log(row.count);
|
|
1020
1031
|
} else if (sql) {
|
|
1021
1032
|
const stmt = db.prepare(sql);
|
|
1022
1033
|
if (stmt.reader) {
|
|
1023
1034
|
const rows = stmt.all();
|
|
1024
|
-
if (
|
|
1035
|
+
if (asJson) {
|
|
1036
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
1037
|
+
} else if (rows.length === 0) {
|
|
1025
1038
|
console.log('(no results)');
|
|
1026
1039
|
} else {
|
|
1027
1040
|
console.table(rows);
|
|
1028
1041
|
}
|
|
1029
1042
|
} else {
|
|
1030
1043
|
const info = stmt.run();
|
|
1031
|
-
console.log(
|
|
1044
|
+
if (asJson) console.log(JSON.stringify({ changes: info.changes, lastInsertRowid: Number(info.lastInsertRowid) }, null, 2));
|
|
1045
|
+
else console.log(`Changes: ${info.changes}`);
|
|
1032
1046
|
}
|
|
1033
1047
|
} else if (repl) {
|
|
1034
1048
|
const readline = require('readline');
|
package/lib/db-commands.js
CHANGED
|
@@ -561,6 +561,12 @@ function cmdLog(args, ctx) {
|
|
|
561
561
|
if (!row) throw new Error(`No llm_log with id ${id}`);
|
|
562
562
|
if (json) return printJson(row);
|
|
563
563
|
|
|
564
|
+
let finishReason = null;
|
|
565
|
+
try {
|
|
566
|
+
const parsed = typeof row.response === 'string' ? JSON.parse(row.response) : row.response;
|
|
567
|
+
if (parsed && typeof parsed.finishReason === 'string') finishReason = parsed.finishReason;
|
|
568
|
+
} catch { /* leave null */ }
|
|
569
|
+
|
|
564
570
|
printRecord(`LLM log ${row.id}`, {
|
|
565
571
|
createdAt: row.createdAt,
|
|
566
572
|
type: row.type,
|
|
@@ -570,6 +576,7 @@ function cmdLog(args, ctx) {
|
|
|
570
576
|
messageId: row.messageId,
|
|
571
577
|
characterId: row.characterId,
|
|
572
578
|
durationMs: row.durationMs,
|
|
579
|
+
finishReason,
|
|
573
580
|
usage: row.usage,
|
|
574
581
|
cacheUsage: row.cacheUsage,
|
|
575
582
|
});
|
|
@@ -636,6 +643,323 @@ function cmdMemories(args, ctx) {
|
|
|
636
643
|
}
|
|
637
644
|
}
|
|
638
645
|
|
|
646
|
+
// ---------- verb: characters ----------
|
|
647
|
+
|
|
648
|
+
// Vault files that the character-properties overlay manages today. Must stay
|
|
649
|
+
// in sync with `CHARACTER_VAULT_DESCRIPTORS` in
|
|
650
|
+
// lib/database/repositories/character-properties-overlay.ts. When the 4.6
|
|
651
|
+
// vault-cutover migration lands, every character is expected to have all of
|
|
652
|
+
// these present.
|
|
653
|
+
const EXPECTED_VAULT_SINGLE_FILES = [
|
|
654
|
+
'properties.json',
|
|
655
|
+
'identity.md',
|
|
656
|
+
'description.md',
|
|
657
|
+
'manifesto.md',
|
|
658
|
+
'personality.md',
|
|
659
|
+
'example-dialogues.md',
|
|
660
|
+
'physical-description.md',
|
|
661
|
+
'physical-prompts.json',
|
|
662
|
+
];
|
|
663
|
+
|
|
664
|
+
function safeJsonArray(raw) {
|
|
665
|
+
if (raw == null || raw === '') return [];
|
|
666
|
+
try {
|
|
667
|
+
const v = JSON.parse(raw);
|
|
668
|
+
return Array.isArray(v) ? v : [];
|
|
669
|
+
} catch {
|
|
670
|
+
return [];
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function normalizeEmpty(v) {
|
|
675
|
+
if (v == null) return '';
|
|
676
|
+
return v;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function inspectCharacterVault(row, mounts) {
|
|
680
|
+
// `flag` / `*Db` / divergence reporting only make sense before the 4.6
|
|
681
|
+
// vault cutover, when the DB still carried the content columns. After
|
|
682
|
+
// the cutover the columns are gone and the vault is the only source of
|
|
683
|
+
// truth — `row` won't carry them. Treat them as null and skip the
|
|
684
|
+
// divergence check; the file-presence count is still useful.
|
|
685
|
+
const preCutover = row.identity !== undefined
|
|
686
|
+
|| row.description !== undefined
|
|
687
|
+
|| row.systemPrompts !== undefined;
|
|
688
|
+
|
|
689
|
+
const status = {
|
|
690
|
+
id: row.id,
|
|
691
|
+
name: row.name,
|
|
692
|
+
flag: row.readPropertiesFromDocumentStore == null
|
|
693
|
+
? null
|
|
694
|
+
: Number(row.readPropertiesFromDocumentStore),
|
|
695
|
+
mountPointId: row.characterDocumentMountPointId || null,
|
|
696
|
+
vault: 'missing',
|
|
697
|
+
presentSingleFiles: 0,
|
|
698
|
+
expectedSingleFiles: EXPECTED_VAULT_SINGLE_FILES.length,
|
|
699
|
+
missingSingleFiles: [],
|
|
700
|
+
promptsVault: 0,
|
|
701
|
+
promptsDb: 0,
|
|
702
|
+
scenariosVault: 0,
|
|
703
|
+
scenariosDb: 0,
|
|
704
|
+
wardrobeVault: 0,
|
|
705
|
+
diverged: [],
|
|
706
|
+
issue: null,
|
|
707
|
+
preCutover,
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
if (preCutover) {
|
|
711
|
+
status.promptsDb = safeJsonArray(row.systemPrompts).length;
|
|
712
|
+
status.scenariosDb = safeJsonArray(row.scenarios).length;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (!row.characterDocumentMountPointId) {
|
|
716
|
+
status.issue = 'no vault';
|
|
717
|
+
return status;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
status.vault = 'present';
|
|
721
|
+
const mountPointId = row.characterDocumentMountPointId;
|
|
722
|
+
|
|
723
|
+
// One-shot listing of every link for this vault; the rest is just lookups.
|
|
724
|
+
const links = mounts.prepare(
|
|
725
|
+
'SELECT relativePath, fileId FROM doc_mount_file_links WHERE mountPointId = ?'
|
|
726
|
+
).all(mountPointId);
|
|
727
|
+
const byPath = new Map();
|
|
728
|
+
for (const link of links) {
|
|
729
|
+
byPath.set(link.relativePath.toLowerCase(), link);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
for (const p of EXPECTED_VAULT_SINGLE_FILES) {
|
|
733
|
+
if (byPath.has(p)) {
|
|
734
|
+
status.presentSingleFiles++;
|
|
735
|
+
} else {
|
|
736
|
+
status.missingSingleFiles.push(p);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
for (const [p] of byPath) {
|
|
741
|
+
if (p.startsWith('prompts/') && p.endsWith('.md')) status.promptsVault++;
|
|
742
|
+
else if (p.startsWith('scenarios/') && p.endsWith('.md')) status.scenariosVault++;
|
|
743
|
+
else if (p.startsWith('wardrobe/') && p.endsWith('.md')) status.wardrobeVault++;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Compare vault contents to DB row for each managed field where the
|
|
747
|
+
// corresponding file is actually present. Only meaningful pre-cutover;
|
|
748
|
+
// post-cutover the DB no longer carries the columns to compare against.
|
|
749
|
+
if (preCutover) {
|
|
750
|
+
const docStmt = mounts.prepare(
|
|
751
|
+
'SELECT content FROM doc_mount_documents WHERE fileId = ?'
|
|
752
|
+
);
|
|
753
|
+
const readVault = (relPath) => {
|
|
754
|
+
const link = byPath.get(relPath);
|
|
755
|
+
if (!link) return null;
|
|
756
|
+
const doc = docStmt.get(link.fileId);
|
|
757
|
+
return doc ? doc.content : null;
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
const mdFields = [
|
|
761
|
+
['identity.md', 'identity'],
|
|
762
|
+
['description.md', 'description'],
|
|
763
|
+
['manifesto.md', 'manifesto'],
|
|
764
|
+
['personality.md', 'personality'],
|
|
765
|
+
['example-dialogues.md', 'exampleDialogues'],
|
|
766
|
+
];
|
|
767
|
+
for (const [vaultPath, dbField] of mdFields) {
|
|
768
|
+
const vault = readVault(vaultPath);
|
|
769
|
+
if (vault === null) continue;
|
|
770
|
+
const db = row[dbField] ?? '';
|
|
771
|
+
if (vault !== db) status.diverged.push(dbField);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const propsRaw = readVault('properties.json');
|
|
775
|
+
if (propsRaw !== null) {
|
|
776
|
+
try {
|
|
777
|
+
const props = JSON.parse(propsRaw);
|
|
778
|
+
const scalarChecks = [
|
|
779
|
+
['pronouns', row.pronouns],
|
|
780
|
+
['title', row.title],
|
|
781
|
+
['firstMessage', row.firstMessage],
|
|
782
|
+
['talkativeness', row.talkativeness],
|
|
783
|
+
];
|
|
784
|
+
for (const [k, dbVal] of scalarChecks) {
|
|
785
|
+
if (normalizeEmpty(props[k]) !== normalizeEmpty(dbVal)) {
|
|
786
|
+
status.diverged.push(k);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
const vaultAliases = JSON.stringify(Array.isArray(props.aliases) ? props.aliases : []);
|
|
790
|
+
const dbAliases = JSON.stringify(safeJsonArray(row.aliases));
|
|
791
|
+
if (vaultAliases !== dbAliases) status.diverged.push('aliases');
|
|
792
|
+
// systemTransparency: tristate (0 / 1 / null), only reported if vault has it
|
|
793
|
+
if (props.systemTransparency !== undefined) {
|
|
794
|
+
if ((props.systemTransparency ?? null) !== (row.systemTransparency ?? null)) {
|
|
795
|
+
status.diverged.push('systemTransparency');
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
} catch {
|
|
799
|
+
status.diverged.push('properties.json:unparseable');
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const physArr = safeJsonArray(row.physicalDescriptions);
|
|
804
|
+
const primary = physArr[0] || null;
|
|
805
|
+
const physMd = readVault('physical-description.md');
|
|
806
|
+
if (physMd !== null) {
|
|
807
|
+
const dbVal = primary && primary.fullDescription != null ? primary.fullDescription : '';
|
|
808
|
+
if (physMd !== dbVal) status.diverged.push('physicalDescription.fullDescription');
|
|
809
|
+
}
|
|
810
|
+
const physJsonRaw = readVault('physical-prompts.json');
|
|
811
|
+
if (physJsonRaw !== null) {
|
|
812
|
+
try {
|
|
813
|
+
const physJson = JSON.parse(physJsonRaw);
|
|
814
|
+
const promptChecks = [
|
|
815
|
+
['short', primary?.shortPrompt],
|
|
816
|
+
['medium', primary?.mediumPrompt],
|
|
817
|
+
['long', primary?.longPrompt],
|
|
818
|
+
['complete', primary?.completePrompt],
|
|
819
|
+
];
|
|
820
|
+
for (const [k, dbVal] of promptChecks) {
|
|
821
|
+
if (normalizeEmpty(physJson[k]) !== normalizeEmpty(dbVal)) {
|
|
822
|
+
status.diverged.push(`physical.${k}Prompt`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
} catch {
|
|
826
|
+
status.diverged.push('physical-prompts.json:unparseable');
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (status.promptsVault !== status.promptsDb) {
|
|
831
|
+
status.diverged.push(`systemPrompts:count(vault=${status.promptsVault},db=${status.promptsDb})`);
|
|
832
|
+
}
|
|
833
|
+
if (status.scenariosVault !== status.scenariosDb) {
|
|
834
|
+
status.diverged.push(`scenarios:count(vault=${status.scenariosVault},db=${status.scenariosDb})`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (status.missingSingleFiles.length === EXPECTED_VAULT_SINGLE_FILES.length) {
|
|
839
|
+
status.issue = 'vault empty';
|
|
840
|
+
} else if (status.missingSingleFiles.length > 0) {
|
|
841
|
+
status.issue = `${status.missingSingleFiles.length} files missing`;
|
|
842
|
+
} else if (status.diverged.length > 0) {
|
|
843
|
+
status.issue = `diverged (${status.diverged.length})`;
|
|
844
|
+
} else if (!preCutover) {
|
|
845
|
+
status.issue = 'ok (post-cutover, vault is canonical)';
|
|
846
|
+
} else if (status.flag === 1) {
|
|
847
|
+
status.issue = 'ok (vault authoritative)';
|
|
848
|
+
} else {
|
|
849
|
+
status.issue = 'ok (db matches vault)';
|
|
850
|
+
}
|
|
851
|
+
return status;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function cmdCharacters(args, ctx) {
|
|
855
|
+
const { flags, positional } = parseSubArgs(args);
|
|
856
|
+
const sub = positional[0] || 'status';
|
|
857
|
+
if (sub !== 'status') {
|
|
858
|
+
throw new Error(`Unknown characters subcommand: ${sub}. Try: status`);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const json = asBool(flags.json);
|
|
862
|
+
const limit = asInt(flags.limit, 0);
|
|
863
|
+
const onlyDiverged = asBool(flags.diverged);
|
|
864
|
+
const onlyBlocked = asBool(flags.blocked);
|
|
865
|
+
const idQuery = flags.id ? String(flags.id) : null;
|
|
866
|
+
|
|
867
|
+
const main = ctx.openMain();
|
|
868
|
+
const mounts = ctx.openMounts();
|
|
869
|
+
try {
|
|
870
|
+
// Probe the schema so this verb works both pre- and post-cutover: after
|
|
871
|
+
// the 4.6 migration the content columns are gone, so we can only ask
|
|
872
|
+
// for what's there.
|
|
873
|
+
const existing = new Set(
|
|
874
|
+
main.prepare('PRAGMA table_info(characters)')
|
|
875
|
+
.all()
|
|
876
|
+
.map(r => r.name)
|
|
877
|
+
);
|
|
878
|
+
const wanted = [
|
|
879
|
+
'id', 'name', 'characterDocumentMountPointId', 'systemTransparency',
|
|
880
|
+
'readPropertiesFromDocumentStore',
|
|
881
|
+
'identity', 'description', 'manifesto', 'personality', 'exampleDialogues',
|
|
882
|
+
'pronouns', 'aliases', 'title', 'firstMessage', 'talkativeness',
|
|
883
|
+
'physicalDescriptions', 'systemPrompts', 'scenarios',
|
|
884
|
+
];
|
|
885
|
+
const cols = wanted.filter(c => existing.has(c));
|
|
886
|
+
let sql = `SELECT ${cols.join(', ')} FROM characters`;
|
|
887
|
+
const params = [];
|
|
888
|
+
if (idQuery) {
|
|
889
|
+
const c = resolveCharacter(main, idQuery);
|
|
890
|
+
sql += ' WHERE id = ?';
|
|
891
|
+
params.push(c.id);
|
|
892
|
+
} else {
|
|
893
|
+
sql += ' ORDER BY name';
|
|
894
|
+
if (limit > 0) {
|
|
895
|
+
sql += ' LIMIT ?';
|
|
896
|
+
params.push(limit);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const rows = main.prepare(sql).all(...params);
|
|
900
|
+
|
|
901
|
+
const all = rows.map(r => inspectCharacterVault(r, mounts));
|
|
902
|
+
const filtered = all.filter(s => {
|
|
903
|
+
if (onlyBlocked && !(s.issue && (s.issue === 'no vault' || s.issue === 'vault empty' || s.issue.endsWith(' files missing')))) {
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
if (onlyDiverged && s.diverged.length === 0 && (!s.missingSingleFiles || s.missingSingleFiles.length === 0)) {
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
return true;
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
if (json) {
|
|
913
|
+
const summary = {
|
|
914
|
+
totalScanned: all.length,
|
|
915
|
+
returned: filtered.length,
|
|
916
|
+
counts: summarizeCharacterStatuses(all),
|
|
917
|
+
characters: filtered,
|
|
918
|
+
};
|
|
919
|
+
return printJson(summary);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const summary = summarizeCharacterStatuses(all);
|
|
923
|
+
const headline = `Scanned ${all.length} character${all.length === 1 ? '' : 's'}: ` +
|
|
924
|
+
`${summary.ok} ok, ${summary.diverged} diverged, ${summary.missingFiles} with missing files, ` +
|
|
925
|
+
`${summary.noVault} with no vault, ${summary.empty} empty.`;
|
|
926
|
+
console.log(headline);
|
|
927
|
+
console.log('');
|
|
928
|
+
printTable(filtered.map(s => ({
|
|
929
|
+
id: s.id.slice(0, 8),
|
|
930
|
+
name: truncate(s.name, 28),
|
|
931
|
+
flag: s.flag == null ? '-' : s.flag,
|
|
932
|
+
vault: s.vault,
|
|
933
|
+
files: s.vault === 'missing' ? '-' : `${s.presentSingleFiles}/${s.expectedSingleFiles}`,
|
|
934
|
+
prompts: s.vault === 'missing' ? '-' : `${s.promptsVault}/${s.promptsDb}`,
|
|
935
|
+
scenarios: s.vault === 'missing' ? '-' : `${s.scenariosVault}/${s.scenariosDb}`,
|
|
936
|
+
wardrobe: s.vault === 'missing' ? '-' : s.wardrobeVault,
|
|
937
|
+
status: truncate(s.issue, 60),
|
|
938
|
+
})));
|
|
939
|
+
|
|
940
|
+
if (filtered.length > 0 && filtered.some(s => s.diverged.length > 0)) {
|
|
941
|
+
console.log('');
|
|
942
|
+
console.log('Run with --json to see the full diverged-field list per character.');
|
|
943
|
+
}
|
|
944
|
+
} finally {
|
|
945
|
+
try { mounts.close(); } catch {}
|
|
946
|
+
try { main.close(); } catch {}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function summarizeCharacterStatuses(all) {
|
|
951
|
+
let ok = 0, diverged = 0, missingFiles = 0, noVault = 0, empty = 0;
|
|
952
|
+
for (const s of all) {
|
|
953
|
+
if (!s.issue) continue;
|
|
954
|
+
if (s.issue.startsWith('ok')) ok++;
|
|
955
|
+
else if (s.issue === 'no vault') noVault++;
|
|
956
|
+
else if (s.issue === 'vault empty') empty++;
|
|
957
|
+
else if (s.issue.endsWith(' files missing')) missingFiles++;
|
|
958
|
+
else if (s.issue.startsWith('diverged')) diverged++;
|
|
959
|
+
}
|
|
960
|
+
return { ok, diverged, missingFiles, noVault, empty };
|
|
961
|
+
}
|
|
962
|
+
|
|
639
963
|
// ---------- verb: optimize ----------
|
|
640
964
|
|
|
641
965
|
const OPTIMIZE_TARGETS = {
|
|
@@ -1105,6 +1429,7 @@ const VERBS = {
|
|
|
1105
1429
|
message: cmdMessage,
|
|
1106
1430
|
log: cmdLog,
|
|
1107
1431
|
memories: cmdMemories,
|
|
1432
|
+
characters: cmdCharacters,
|
|
1108
1433
|
optimize: cmdOptimize,
|
|
1109
1434
|
backup: cmdBackup,
|
|
1110
1435
|
integrity: cmdIntegrity,
|