@wirecat/cli-core 0.488.0 → 0.490.0
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/CHANGELOG.md +112 -0
- package/dist/calendar/cli/index.js +48 -13
- package/dist/calendar/index.d.ts +1 -1
- package/dist/calendar/mcp/index.d.ts +390 -0
- package/dist/calendar/mcp/index.js +50 -12
- package/dist/calendar/reads.d.ts +9 -2
- package/dist/calendar/reads.js +60 -5
- package/dist/calendar/recurrence.d.ts +20 -0
- package/dist/calendar/recurrence.js +74 -0
- package/dist/calendar/sync.d.ts +6 -0
- package/dist/calendar/sync.js +123 -41
- package/dist/calendar/testing/memory.js +90 -7
- package/dist/calendar/testing/sync-contract.js +90 -0
- package/dist/calendar/testing/writer-contract.d.ts +2 -6
- package/dist/calendar/testing/writer-contract.js +157 -29
- package/dist/calendar/testing/writer.d.ts +3 -1
- package/dist/calendar/testing/writer.js +11 -0
- package/dist/calendar/writer.d.ts +20 -3
- package/dist/calendar/writer.js +8 -4
- package/dist/calendar/writes.d.ts +21 -12
- package/dist/calendar/writes.js +259 -20
- package/dist/errors/reasons.d.ts +1 -1
- package/dist/errors/reasons.js +5 -0
- package/dist/meetings/event-store.d.ts +80 -2
- package/dist/meetings/event-store.js +65 -1
- package/dist/meetings/index.d.ts +2 -1
- package/dist/meetings/index.js +1 -0
- package/dist/meetings/testing/event-store-contract.d.ts +1 -1
- package/dist/meetings/testing/event-store-contract.js +114 -0
- package/dist/store/drivers/node-sqlite.d.ts +9 -1
- package/dist/store/drivers/node-sqlite.js +30 -8
- package/dist/store/sqlite/calendar-events.d.ts +4 -0
- package/dist/store/sqlite/calendar-events.js +89 -0
- package/dist/store/sqlite/calendar.js +126 -94
- package/dist/store/sqlite/identities.d.ts +1 -1
- package/dist/store/sqlite/identities.js +4 -1
- package/dist/store/sqlite/knowledge.js +4 -2
- package/dist/store/sqlite/manifest.js +1 -0
- package/dist/store/sqlite/migrations.generated.js +10 -0
- package/dist/store/sqlite/note-index.d.ts +16 -1
- package/dist/store/sqlite/note-index.js +68 -3
- package/dist/store/sqlite/notes.js +7 -5
- package/package.json +1 -1
|
@@ -53,6 +53,7 @@ export const MANIFEST = [
|
|
|
53
53
|
{ name: "20261029224106_version-38-folded-names", version: 38, minCompatible: 34 },
|
|
54
54
|
{ name: "20261029224107_version-38-folded-triggers", version: 38, minCompatible: 34 },
|
|
55
55
|
{ name: "20261030114008_version-39-email-locations", version: 39, minCompatible: 34 },
|
|
56
|
+
{ name: "20261030225520_version-40-note-trigrams", version: 40, minCompatible: 34 },
|
|
56
57
|
];
|
|
57
58
|
export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
|
|
58
59
|
const migrations = [];
|
|
@@ -837,5 +837,15 @@ export const GENERATED = [
|
|
|
837
837
|
"CREATE TABLE `email_locations` (\n\t`mailbox_id` integer NOT NULL,\n\t`uidvalidity` text NOT NULL,\n\t`uid` integer NOT NULL,\n\t`email_id` integer NOT NULL,\n\tCONSTRAINT `email_locations_pk` PRIMARY KEY(`mailbox_id`, `uidvalidity`, `uid`),\n\tCONSTRAINT `fk_email_locations_mailbox_id_mailboxes_id_fk` FOREIGN KEY (`mailbox_id`) REFERENCES `mailboxes`(`id`),\n\tCONSTRAINT `fk_email_locations_email_id_emails_id_fk` FOREIGN KEY (`email_id`) REFERENCES `emails`(`id`)\n);",
|
|
838
838
|
"CREATE INDEX `email_locations_by_email_id` ON `email_locations` (`email_id`);"
|
|
839
839
|
]
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
"name": "20261030225520_version-40-note-trigrams",
|
|
843
|
+
"statements": [
|
|
844
|
+
"CREATE VIRTUAL TABLE note_trigrams USING fts5(text, content = '', contentless_delete = 1, tokenize = 'trigram');",
|
|
845
|
+
"CREATE VIRTUAL TABLE document_trigrams USING fts5(text, content = '', contentless_delete = 1, tokenize = 'trigram');",
|
|
846
|
+
"CREATE TRIGGER note_trigrams_bd BEFORE DELETE ON notes BEGIN\n DELETE FROM note_trigrams WHERE rowid = old.id;\nEND;",
|
|
847
|
+
"CREATE TRIGGER document_trigrams_bd BEFORE DELETE ON documents BEGIN\n DELETE FROM document_trigrams WHERE rowid = old.id;\nEND;",
|
|
848
|
+
"-- The trigrams hold JS-normalized text, which SQL cannot produce: fillTrigrams fills the rows stored before this\n-- version, up to `watermark`, and a search reads the rest of them by their text meanwhile.\nINSERT INTO search_index_state (name, watermark, filled_through, terms_through, normalizer_version)\n SELECT 'note_trigrams', coalesce(max(id), 0), 0, 0, 1 FROM notes\n UNION ALL SELECT 'document_trigrams', coalesce(max(id), 0), 0, 0, 1 FROM documents;"
|
|
849
|
+
]
|
|
840
850
|
}
|
|
841
851
|
];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Stemmer, type Stemmers } from "../../search/stem.js";
|
|
2
2
|
import type { CacheDatabase } from "../driver.js";
|
|
3
|
+
import type { Where } from "./query-filter.js";
|
|
3
4
|
export interface NoteIndexState {
|
|
4
5
|
/** Rows written since they were indexed. */
|
|
5
6
|
pending: number;
|
|
@@ -102,6 +103,20 @@ export declare const CORPORA: {
|
|
|
102
103
|
};
|
|
103
104
|
/** What a row is indexed as: its title, then its text. Chunk offsets point into this. */
|
|
104
105
|
export declare const noteIndexText: (title: string | null, text: string) => string;
|
|
106
|
+
/**
|
|
107
|
+
* `column` (a note's or document's id) is a row whose title or text holds `search` anywhere, in any letter case;
|
|
108
|
+
* no condition for an empty `search`. A row the trigrams do not hold yet — waiting in its queue, or stored before
|
|
109
|
+
* version 40 and not yet reached by `fillTrigrams` — is matched by its text as SQLite's `lower()` reads it.
|
|
110
|
+
*/
|
|
111
|
+
export declare const holdingText: (type: "note" | "document", column: string, search: string) => Where | undefined;
|
|
112
|
+
/**
|
|
113
|
+
* Writes the trigrams of the notes and documents stored before version 40, in short write transactions, up to the
|
|
114
|
+
* highest id the migration found; later rows get theirs from the drain. Stopping between batches loses nothing.
|
|
115
|
+
*/
|
|
116
|
+
export declare const fillTrigrams: (database: CacheDatabase, { batch, until }?: {
|
|
117
|
+
batch?: number;
|
|
118
|
+
until?: () => boolean;
|
|
119
|
+
}) => number;
|
|
105
120
|
export declare const corpusIndexState: (database: CacheDatabase, corpus: Corpus) => NoteIndexState;
|
|
106
121
|
/** Documents and notes together: what a notes search reads. */
|
|
107
122
|
export declare const noteIndexState: (database: CacheDatabase) => NoteIndexState;
|
|
@@ -113,7 +128,7 @@ export declare const drainCorpus: (database: CacheDatabase, corpus: Corpus, stem
|
|
|
113
128
|
batch?: number;
|
|
114
129
|
until?: () => boolean;
|
|
115
130
|
}) => number;
|
|
116
|
-
/** Documents, then notes: what a notes search reads. */
|
|
131
|
+
/** Documents, then notes: what a notes search reads; then the trigrams still to fill. */
|
|
117
132
|
export declare const drainNoteIndex: (database: CacheDatabase, stemmerFor: (stemmers: Stemmers) => Stemmer, options?: {
|
|
118
133
|
batch?: number;
|
|
119
134
|
until?: () => boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
1
2
|
import { CHUNK_CHARS, chunkHash, splitSections, splitText } from "../../conversations/chunks.js";
|
|
2
3
|
import { analyzerIdentity, DEFAULT_STEMMERS } from "../../search/stem.js";
|
|
3
4
|
import { taskStemsOf, taskWordsOf } from "../../search/task-similarity.js";
|
|
@@ -103,6 +104,62 @@ export const CORPORA = {
|
|
|
103
104
|
/** What a row is indexed as: its title, then its text. Chunk offsets point into this. */
|
|
104
105
|
export const noteIndexText = (title, text) => (title ? `${title}\n\n${text}` : text);
|
|
105
106
|
const queuedAs = (corpus) => corpus.queued ?? corpus.type;
|
|
107
|
+
const hasTrigrams = (corpus) => corpus.type === "note" || corpus.type === "document";
|
|
108
|
+
/**
|
|
109
|
+
* `column` (a note's or document's id) is a row whose title or text holds `search` anywhere, in any letter case;
|
|
110
|
+
* no condition for an empty `search`. A row the trigrams do not hold yet — waiting in its queue, or stored before
|
|
111
|
+
* version 40 and not yet reached by `fillTrigrams` — is matched by its text as SQLite's `lower()` reads it.
|
|
112
|
+
*/
|
|
113
|
+
export const holdingText = (type, column, search) => {
|
|
114
|
+
const text = normalize(search);
|
|
115
|
+
if (text === "")
|
|
116
|
+
return undefined;
|
|
117
|
+
// A trigram index answers a shorter query with nothing, silently.
|
|
118
|
+
if ([...text].length < 3)
|
|
119
|
+
throw new CliError("validation_error", "search needs at least three characters");
|
|
120
|
+
const queued = `SELECT id FROM ${type}_index_pending WHERE indexable_type = '${type}'`;
|
|
121
|
+
const state = (field) => `(SELECT ${field} FROM search_index_state WHERE name = '${type}_trigrams')`;
|
|
122
|
+
return {
|
|
123
|
+
sql: `${column} IN (SELECT rowid FROM ${type}_trigrams WHERE ${type}_trigrams MATCH ? AND rowid NOT IN (${queued}) ` +
|
|
124
|
+
`UNION ALL SELECT r.id FROM ${type}s r WHERE (r.id IN (${queued}) OR (r.id > ${state("filled_through")} ` +
|
|
125
|
+
`AND r.id <= ${state("watermark")})) AND instr(lower(coalesce(r.body, '')), lower(?)) > 0)`,
|
|
126
|
+
params: [`"${text.replaceAll('"', '""')}"`, search],
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Writes the trigrams of the notes and documents stored before version 40, in short write transactions, up to the
|
|
131
|
+
* highest id the migration found; later rows get theirs from the drain. Stopping between batches loses nothing.
|
|
132
|
+
*/
|
|
133
|
+
export const fillTrigrams = (database, { batch = 200, until = () => false } = {}) => {
|
|
134
|
+
let filled = 0;
|
|
135
|
+
for (const type of ["document", "note"]) {
|
|
136
|
+
const name = `${type}_trigrams`;
|
|
137
|
+
const state = database.prepare("SELECT watermark, filled_through FROM search_index_state WHERE name = ?");
|
|
138
|
+
const rows = database.prepare(`SELECT id, title, coalesce(body, '') AS body FROM ${type}s WHERE id > ? AND id <= ? AND deleted_at IS NULL ` +
|
|
139
|
+
"ORDER BY id LIMIT ?");
|
|
140
|
+
const drop = database.prepare(`DELETE FROM ${name} WHERE rowid = ?`);
|
|
141
|
+
const add = database.prepare(`INSERT INTO ${name} (rowid, text) VALUES (?, ?)`);
|
|
142
|
+
const reached = database.prepare("UPDATE search_index_state SET filled_through = ? WHERE name = ?");
|
|
143
|
+
for (;;) {
|
|
144
|
+
const at = state.get(name);
|
|
145
|
+
const mark = Number(at?.watermark ?? 0);
|
|
146
|
+
if (Number(at?.filled_through ?? mark) >= mark || until())
|
|
147
|
+
break;
|
|
148
|
+
filled += write(database, () => {
|
|
149
|
+
const page = rows.all(Number(at?.filled_through), mark, batch);
|
|
150
|
+
for (const row of page) {
|
|
151
|
+
const text = normalize(noteIndexText(row.title == null ? null : String(row.title), String(row.body)));
|
|
152
|
+
drop.run(Number(row.id));
|
|
153
|
+
if (text !== "")
|
|
154
|
+
add.run(Number(row.id), text);
|
|
155
|
+
}
|
|
156
|
+
reached.run(page.length < batch ? mark : Number(page.at(-1)?.id), name);
|
|
157
|
+
return page.length;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return filled;
|
|
162
|
+
};
|
|
106
163
|
export const corpusIndexState = (database, corpus) => {
|
|
107
164
|
const row = indexRow(database, corpus.index);
|
|
108
165
|
const pending = Number(database
|
|
@@ -139,9 +196,13 @@ const writer = (database, corpus, stemmer) => {
|
|
|
139
196
|
const statements = {
|
|
140
197
|
dropWords: database.prepare(`DELETE FROM ${corpus.type}_words WHERE rowid = ?`),
|
|
141
198
|
dropStems: database.prepare(`DELETE FROM ${corpus.type}_stems WHERE rowid = ?`),
|
|
199
|
+
dropTrigrams: hasTrigrams(corpus) ? database.prepare(`DELETE FROM ${corpus.type}_trigrams WHERE rowid = ?`) : null,
|
|
142
200
|
dropChunks: database.prepare("DELETE FROM chunks WHERE chunkable_type = ? AND chunkable_id = ?"),
|
|
143
201
|
words: database.prepare(`INSERT INTO ${corpus.type}_words (rowid, normalized_text, scope) VALUES (?, ?, ?)`),
|
|
144
202
|
stems: database.prepare(`INSERT INTO ${corpus.type}_stems (rowid, stems, scope) VALUES (?, ?, ?)`),
|
|
203
|
+
trigrams: hasTrigrams(corpus)
|
|
204
|
+
? database.prepare(`INSERT INTO ${corpus.type}_trigrams (rowid, text) VALUES (?, ?)`)
|
|
205
|
+
: null,
|
|
145
206
|
chunk: database.prepare("INSERT INTO chunks (chunkable_type, chunkable_id, position, start_offset, end_offset, content_hash, scope, account_id, created_at, updated_at) " +
|
|
146
207
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
|
|
147
208
|
dequeue: database.prepare(`DELETE FROM ${corpus.type}_index_pending WHERE indexable_type = ? AND id = ?`),
|
|
@@ -151,14 +212,17 @@ const writer = (database, corpus, stemmer) => {
|
|
|
151
212
|
const before = oldHashes.all(corpus.type, id).map((hash) => String(hash.hash));
|
|
152
213
|
statements.dropWords.run(id);
|
|
153
214
|
statements.dropStems.run(id);
|
|
215
|
+
statements.dropTrigrams?.run(id);
|
|
154
216
|
statements.dropChunks.run(corpus.type, id);
|
|
155
217
|
const live = row !== undefined && Number(row.live) === 1;
|
|
156
218
|
const text = live ? noteIndexText(row.title == null ? null : String(row.title), String(row.body)) : "";
|
|
157
219
|
const scope = live ? String(row.scope ?? "personal") : "";
|
|
158
220
|
const terms = corpus.terms && text ? corpus.terms(text, stemmer) : undefined;
|
|
159
221
|
const words = terms ? terms.words : normalize(text);
|
|
160
|
-
if (words !== "")
|
|
222
|
+
if (words !== "") {
|
|
161
223
|
statements.words.run(id, words, scope);
|
|
224
|
+
statements.trigrams?.run(id, words);
|
|
225
|
+
}
|
|
162
226
|
const stems = terms ? terms.stems : text ? stemmer.indexText(text) : "";
|
|
163
227
|
if (stems !== "")
|
|
164
228
|
statements.stems.run(id, stems, scope);
|
|
@@ -228,9 +292,10 @@ export const drainCorpus = (database, corpus, stemmerFor, { batch = 200, until =
|
|
|
228
292
|
}
|
|
229
293
|
return done;
|
|
230
294
|
};
|
|
231
|
-
/** Documents, then notes: what a notes search reads. */
|
|
295
|
+
/** Documents, then notes: what a notes search reads; then the trigrams still to fill. */
|
|
232
296
|
export const drainNoteIndex = (database, stemmerFor, options = {}) => drainCorpus(database, CORPORA.document, stemmerFor, options) +
|
|
233
|
-
drainCorpus(database, CORPORA.note, stemmerFor, options)
|
|
297
|
+
drainCorpus(database, CORPORA.note, stemmerFor, options) +
|
|
298
|
+
fillTrigrams(database, options);
|
|
234
299
|
/** Queues every document, note and memory again — for `store rebuild`. */
|
|
235
300
|
export const resetNoteIndex = (database) => {
|
|
236
301
|
write(database, () => {
|
|
@@ -12,6 +12,7 @@ import { PREFERRED_FILE, syncFile } from "./files.js";
|
|
|
12
12
|
import { importedDocumentWhere } from "./imported-documents.js";
|
|
13
13
|
import { LINK_KINDS, LINK_ORIGINS, linkKind } from "./link-kinds.js";
|
|
14
14
|
import { reviewLink } from "./link-reviews.js";
|
|
15
|
+
import { holdingText } from "./note-index.js";
|
|
15
16
|
import { addTags, ensureTag, labelsOf } from "./tags.js";
|
|
16
17
|
import { canonicalThingReference, referenceOfThing, storedThing, thingOf } from "./things.js";
|
|
17
18
|
import { write } from "./transaction.js";
|
|
@@ -525,7 +526,8 @@ export const notesStoreOver = (context) => {
|
|
|
525
526
|
if (options.about !== undefined && !about)
|
|
526
527
|
return { items: [], hasMore: false };
|
|
527
528
|
const folder = options.folderId === undefined ? null : Number(folderRow(options.folderId).id);
|
|
528
|
-
const
|
|
529
|
+
const documentText = options.search === undefined ? undefined : holdingText("document", "d.id", options.search);
|
|
530
|
+
const noteText = options.search === undefined ? undefined : holdingText("note", "n.id", options.search);
|
|
529
531
|
const aboutType = about?.type ?? null;
|
|
530
532
|
const aboutId = about?.id ?? null;
|
|
531
533
|
const files = options.source === "internal"
|
|
@@ -541,16 +543,16 @@ export const notesStoreOver = (context) => {
|
|
|
541
543
|
"WHERE f.deleted_at IS NULL AND d.deleted_at IS NULL AND f.account_id = ? AND ? IS NOT NULL ") +
|
|
542
544
|
"AND coalesce(json_extract(d.metadata, '$.catalog.active'), 1)=1 " +
|
|
543
545
|
`AND (? IS NULL OR EXISTS (SELECT 1 FROM links l WHERE l.from_type = 'document' AND l.from_id = d.id AND l.kind = ${linkKind("about")} AND l.to_type = ? AND l.to_id = ?)) ` +
|
|
544
|
-
|
|
545
|
-
.all(folder, folder, aboutType, aboutType, aboutId,
|
|
546
|
+
`${documentText ? `AND ${documentText.sql} ` : ""}ORDER BY d.created_at DESC, d.id DESC LIMIT ?`)
|
|
547
|
+
.all(folder, folder, aboutType, aboutType, aboutId, ...(documentText?.params ?? []), limit + offset + 1)
|
|
546
548
|
.map(documentOf);
|
|
547
549
|
const written = options.source === "file" || folder !== null
|
|
548
550
|
? []
|
|
549
551
|
: database
|
|
550
552
|
.prepare("SELECT n.* FROM notes n WHERE n.deleted_at IS NULL " +
|
|
551
553
|
`AND (? IS NULL OR (n.notable_type = ? AND n.notable_id = ?) OR EXISTS (SELECT 1 FROM links l WHERE l.from_type = 'note' AND l.from_id = n.id AND l.kind = ${linkKind("about")} AND l.to_type = ? AND l.to_id = ?)) ` +
|
|
552
|
-
|
|
553
|
-
.all(aboutType, aboutType, aboutId, aboutType, aboutId,
|
|
554
|
+
`${noteText ? `AND ${noteText.sql} ` : ""}ORDER BY n.created_at DESC, n.id DESC LIMIT ?`)
|
|
555
|
+
.all(aboutType, aboutType, aboutId, aboutType, aboutId, ...(noteText?.params ?? []), limit + offset + 1)
|
|
554
556
|
.map(noteOf);
|
|
555
557
|
const all = [...files, ...written].sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.ref.localeCompare(a.ref));
|
|
556
558
|
return { items: all.slice(offset, offset + limit), hasMore: all.length > offset + limit };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.490.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|