@wirecat/cli-core 0.379.0 → 0.381.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 +85 -0
- package/dist/cli/bot/watch.js +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/meeting-preparation-answer.d.ts +5 -0
- package/dist/cli/meeting-preparation-projects.d.ts +5 -0
- package/dist/cli/meeting-preparation-read.d.ts +5 -0
- package/dist/cli/messenger/chats-stats-command.js +5 -1
- package/dist/cli/messenger/contacts-command.js +8 -4
- package/dist/cli/messenger/stats-answers.d.ts +2 -1
- package/dist/cli/messenger/stats-answers.js +1 -1
- package/dist/cli/messenger/stats-command.js +4 -9
- package/dist/cli/messenger/stored.js +12 -7
- package/dist/cli/messenger/watch-command.js +6 -0
- package/dist/domain/models.d.ts +23 -2
- package/dist/entities/index.d.ts +15 -0
- package/dist/entities/meeting-preparation.d.ts +5 -0
- package/dist/errors/reasons.js +1 -0
- package/dist/mcp/meeting-preparation.d.ts +5 -0
- package/dist/mcp/tools/stats.js +9 -6
- package/dist/meetings/answers.d.ts +6 -0
- package/dist/meetings/answers.js +5 -0
- package/dist/meetings/cli/index.d.ts +2 -0
- package/dist/meetings/cli/index.js +5 -2
- package/dist/meetings/cli/search.js +1 -1
- package/dist/meetings/hit-query.d.ts +12 -0
- package/dist/meetings/hit-query.js +58 -0
- package/dist/meetings/import.d.ts +2 -0
- package/dist/meetings/import.js +9 -2
- package/dist/meetings/index.d.ts +2 -0
- package/dist/meetings/index.js +2 -0
- package/dist/meetings/mcp/search.d.ts +6 -0
- package/dist/meetings/mcp/search.js +5 -1
- package/dist/meetings/project-evidence-answer.d.ts +5 -0
- package/dist/meetings/pull.d.ts +2 -0
- package/dist/meetings/recording-schedule.js +6 -5
- package/dist/meetings/search-store.d.ts +3 -1
- package/dist/meetings/store.d.ts +13 -0
- package/dist/meetings/transcript-text.d.ts +20 -0
- package/dist/meetings/transcript-text.js +59 -0
- package/dist/ports/messenger.d.ts +10 -3
- package/dist/render/markdown.js +2 -2
- package/dist/resolve.d.ts +6 -0
- package/dist/resolve.js +13 -4
- package/dist/search/garbled.d.ts +30 -0
- package/dist/search/garbled.js +130 -0
- package/dist/search/russian-dictionary.d.ts +5 -0
- package/dist/search/russian-dictionary.js +66 -0
- package/dist/services/meeting-project-discovery.js +2 -2
- package/dist/services/moment.d.ts +21 -0
- package/dist/services/moment.js +19 -0
- package/dist/services/retention.js +6 -3
- package/dist/store/messages.d.ts +16 -3
- package/dist/store/messages.js +7 -1
- package/dist/store/sqlite/accounts.js +2 -1
- package/dist/store/sqlite/chat-rekey.js +9 -1
- package/dist/store/sqlite/chats.js +2 -1
- package/dist/store/sqlite/manifest.js +1 -0
- package/dist/store/sqlite/meeting-hit-query.d.ts +60 -0
- package/dist/store/sqlite/meeting-hit-query.js +262 -0
- package/dist/store/sqlite/meeting-hit-search.js +28 -45
- package/dist/store/sqlite/meetings.js +56 -1
- package/dist/store/sqlite/messages.d.ts +11 -3
- package/dist/store/sqlite/messages.js +74 -3
- package/dist/store/sqlite/migrations.generated.js +7 -0
- package/dist/store/sqlite/open-store.js +16 -5
- package/dist/store/sqlite/schema.d.ts +155 -0
- package/dist/store/sqlite/schema.js +22 -0
- package/package.json +4 -3
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { CliError } from "@wirecat/cli-base";
|
|
3
|
-
import { formatMeetingReference } from "../../meetings/index.js";
|
|
4
|
-
import {
|
|
3
|
+
import { formatMeetingReference, parseMeetingQuery } from "../../meetings/index.js";
|
|
4
|
+
import { matchMeetingText, meetingQuerySql, resolveMeetingQuery } from "./meeting-hit-query.js";
|
|
5
5
|
import { indexedText } from "./meeting-index-values.js";
|
|
6
6
|
import { checkMeetingCancelled } from "./meeting-operation.js";
|
|
7
7
|
import { positiveReadInteger } from "./meeting-reads.js";
|
|
8
|
-
import {
|
|
8
|
+
import { meetingStemState } from "./meeting-stem-state.js";
|
|
9
9
|
import { read } from "./transaction.js";
|
|
10
10
|
const fail = (message) => {
|
|
11
11
|
throw new CliError("validation_error", message);
|
|
@@ -42,8 +42,9 @@ const cursorOf = (value, key) => {
|
|
|
42
42
|
catch {
|
|
43
43
|
return fail("Invalid meeting search cursor");
|
|
44
44
|
}
|
|
45
|
-
if (parsed?.version !==
|
|
45
|
+
if (parsed?.version !== 2 ||
|
|
46
46
|
parsed.key !== key ||
|
|
47
|
+
![0, 1, 2].includes(parsed.tier) ||
|
|
47
48
|
!Number.isSafeInteger(parsed.started) ||
|
|
48
49
|
Math.abs(parsed.started) > 8640000000000000 ||
|
|
49
50
|
!Number.isSafeInteger(parsed.meeting) ||
|
|
@@ -86,21 +87,14 @@ export function meetingHitSearchOver({ database }) {
|
|
|
86
87
|
fail("Meeting search limit must be from 1 through 1000");
|
|
87
88
|
if (!Number.isSafeInteger(maxReadBytes) || maxReadBytes < 1 || maxReadBytes > 64 * 1024 ** 2)
|
|
88
89
|
fail("Meeting search byte limit must be from 1 through 67108864");
|
|
89
|
-
if (typeof rawQuery
|
|
90
|
+
if (typeof rawQuery === "string" && Buffer.byteLength(rawQuery) > 65536)
|
|
90
91
|
fail("Meeting search query must be a bounded string");
|
|
91
|
-
const
|
|
92
|
-
if (rawWords.length > 128)
|
|
93
|
-
fail("Meeting search supports at most 128 query words");
|
|
92
|
+
const ast = typeof rawQuery === "string" ? parseMeetingQuery(rawQuery) : rawQuery;
|
|
94
93
|
return read(database, () => {
|
|
95
94
|
checkMeetingCancelled(input.signal);
|
|
96
95
|
const state = meetingStemState(database, input.accountId);
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
stems: state.stemmer && raw.length <= 1024
|
|
100
|
-
? [...new Set(state.stemmer.explain(raw).map((word) => word.stem))]
|
|
101
|
-
: [],
|
|
102
|
-
}));
|
|
103
|
-
const key = fingerprint(JSON.stringify(terms), input, state.identity);
|
|
96
|
+
const tree = resolveMeetingQuery(database, input.accountId, ast, state);
|
|
97
|
+
const key = fingerprint(JSON.stringify(tree), input, state.identity);
|
|
104
98
|
const after = cursorOf(input.after, key);
|
|
105
99
|
const pending = database
|
|
106
100
|
.prepare(`SELECT 1 FROM meeting_index_pending p
|
|
@@ -118,20 +112,8 @@ export function meetingHitSearchOver({ database }) {
|
|
|
118
112
|
indexing: pending || !state.stemmer || state.through < state.watermark ? "pending" : "ready",
|
|
119
113
|
indexCompleteness: "unknown",
|
|
120
114
|
};
|
|
121
|
-
if (terms.length === 0)
|
|
122
|
-
return { items: [], hasMore: false, nextCursor: null, coverage };
|
|
123
115
|
const clauses = ["m.account_id=?", "m.deleted_at IS NULL"];
|
|
124
|
-
const queryParams =
|
|
125
|
-
const termTables = terms.map((term, i) => {
|
|
126
|
-
queryParams.push(`normalized_text : ("${term.literal}"*)`);
|
|
127
|
-
let select = "SELECT rowid AS rid FROM meeting_words WHERE meeting_words MATCH ?";
|
|
128
|
-
if (state.stemmer && state.through > 0 && term.stems.length) {
|
|
129
|
-
queryParams.push(`stems : (${term.stems.map((stem) => `"${stem.replaceAll('"', '""')}"`).join(" OR ")})`, state.through);
|
|
130
|
-
select += " UNION SELECT rowid AS rid FROM meeting_stems WHERE meeting_stems MATCH ? AND rowid<=?";
|
|
131
|
-
}
|
|
132
|
-
return `term_${i} AS (${select})`;
|
|
133
|
-
});
|
|
134
|
-
const matching = terms.map((_, i) => `SELECT rid FROM term_${i}`).join(" INTERSECT ");
|
|
116
|
+
const { sql: matching, params: queryParams } = meetingQuerySql(tree, state);
|
|
135
117
|
const params = [input.accountId];
|
|
136
118
|
if (input.meetingId !== undefined) {
|
|
137
119
|
clauses.push("m.id=?");
|
|
@@ -170,17 +152,17 @@ export function meetingHitSearchOver({ database }) {
|
|
|
170
152
|
params.push(input.tagId);
|
|
171
153
|
}
|
|
172
154
|
const seek = after
|
|
173
|
-
? "WHERE h.started<? OR (h.started=? AND (h.meeting_id>? OR (h.meeting_id=? AND (h.kind>? OR (h.kind=? AND h.id>?)))))"
|
|
155
|
+
? "WHERE h.tier>? OR (h.tier=? AND (h.started<? OR (h.started=? AND (h.meeting_id>? OR (h.meeting_id=? AND (h.kind>? OR (h.kind=? AND h.id>?)))))))"
|
|
174
156
|
: "";
|
|
175
157
|
if (after)
|
|
176
|
-
params.push(after.started, after.started, after.meeting, after.meeting, after.kind, after.kind, after.id);
|
|
177
|
-
const sql = `WITH
|
|
178
|
-
SELECT m.started,m.id AS meeting_id,0 AS kind,r.id,r.start_ms,${bytes("r", ["text"])} AS bytes,t.id AS transcript_id,r.position FROM matching h JOIN meeting_transcript_rows r ON h.rid%4=1 AND r.id=h.rid/4 JOIN meeting_transcripts t ON t.id=r.meeting_transcript_id AND t.superseded_at IS NULL AND t.deleted_at IS NULL JOIN selected m ON m.id=t.meeting_id
|
|
179
|
-
UNION ALL SELECT m.started,m.id,1,c.id,NULL,${bytes("c", ["text"])},NULL,NULL FROM matching h JOIN meeting_chat_messages c ON h.rid%4=2 AND c.id=h.rid/4 JOIN selected m ON m.id=c.meeting_id
|
|
180
|
-
UNION ALL SELECT m.started,m.id,2,s.id,NULL,${bytes("s", fields)},NULL,NULL FROM matching h JOIN meeting_summaries s ON h.rid%4=3 AND s.id=h.rid/4 JOIN selected m ON m.id=s.meeting_id)
|
|
158
|
+
params.push(after.tier, after.tier, after.started, after.started, after.meeting, after.meeting, after.kind, after.kind, after.id);
|
|
159
|
+
const sql = `WITH matches AS (${matching}), matching AS (SELECT rid,tier FROM matches h WHERE NOT EXISTS(SELECT 1 FROM meeting_index_pending p WHERE p.id=h.rid/4 AND p.indexable_type=CASE h.rid%4 WHEN 1 THEN 'meeting_transcript_row' WHEN 2 THEN 'meeting_chat_message' ELSE 'meeting_summary' END)), selected AS (SELECT m.id,coalesce(m.started_at,0) AS started FROM meetings m WHERE ${clauses.join(" AND ")}), hits AS (
|
|
160
|
+
SELECT h.tier,m.started,m.id AS meeting_id,0 AS kind,r.id,r.start_ms,${bytes("r", ["text"])} AS bytes,t.id AS transcript_id,r.position FROM matching h JOIN meeting_transcript_rows r ON h.rid%4=1 AND r.id=h.rid/4 JOIN meeting_transcripts t ON t.id=r.meeting_transcript_id AND t.superseded_at IS NULL AND t.deleted_at IS NULL JOIN selected m ON m.id=t.meeting_id
|
|
161
|
+
UNION ALL SELECT h.tier,m.started,m.id,1,c.id,NULL,${bytes("c", ["text"])},NULL,NULL FROM matching h JOIN meeting_chat_messages c ON h.rid%4=2 AND c.id=h.rid/4 JOIN selected m ON m.id=c.meeting_id
|
|
162
|
+
UNION ALL SELECT h.tier,m.started,m.id,2,s.id,NULL,${bytes("s", fields)},NULL,NULL FROM matching h JOIN meeting_summaries s ON h.rid%4=3 AND s.id=h.rid/4 JOIN selected m ON m.id=s.meeting_id)
|
|
181
163
|
SELECT h.*,${bytes("m", ["title"])}+${bytes("s", ["doc_url"])} AS context_bytes FROM hits h
|
|
182
164
|
JOIN meetings m ON m.id=h.meeting_id LEFT JOIN meeting_summaries s ON h.kind=2 AND s.id=h.id
|
|
183
|
-
${seek} ORDER BY h.started DESC,h.meeting_id,h.kind,h.id LIMIT ?`;
|
|
165
|
+
${seek} ORDER BY h.tier,h.started DESC,h.meeting_id,h.kind,h.id LIMIT ?`;
|
|
184
166
|
const rows = database.prepare(sql).all(...queryParams, ...params, limit + 1);
|
|
185
167
|
let total = 0;
|
|
186
168
|
const meetingMetadata = database.prepare("SELECT title,started_at FROM meetings WHERE id=? AND account_id=? AND deleted_at IS NULL");
|
|
@@ -232,15 +214,15 @@ export function meetingHitSearchOver({ database }) {
|
|
|
232
214
|
startMs: row.start_ms === null ? null : Number(row.start_ms),
|
|
233
215
|
};
|
|
234
216
|
});
|
|
235
|
-
const fresh = items.
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
217
|
+
const fresh = items.flatMap((item) => {
|
|
218
|
+
const corrections = matchMeetingText(tree, item.text, state.stemmer);
|
|
219
|
+
if (!corrections)
|
|
220
|
+
return [];
|
|
221
|
+
return [
|
|
222
|
+
corrections.length
|
|
223
|
+
? { ...item, match: "corrected", corrections }
|
|
224
|
+
: { ...item, match: "words" },
|
|
225
|
+
];
|
|
244
226
|
});
|
|
245
227
|
if (fresh.length !== items.length)
|
|
246
228
|
coverage.indexing = "pending";
|
|
@@ -248,8 +230,9 @@ export function meetingHitSearchOver({ database }) {
|
|
|
248
230
|
const hasMore = rows.length > limit;
|
|
249
231
|
const nextCursor = hasMore && last
|
|
250
232
|
? Buffer.from(JSON.stringify({
|
|
251
|
-
version:
|
|
233
|
+
version: 2,
|
|
252
234
|
key,
|
|
235
|
+
tier: Number(last.tier),
|
|
253
236
|
started: Number(last.started),
|
|
254
237
|
meeting: Number(last.meeting_id),
|
|
255
238
|
kind: Number(last.kind),
|
|
@@ -166,14 +166,65 @@ export const saveAttachment = (database, type, parentId, a, now) => upsert(datab
|
|
|
166
166
|
content_sha256: a.contentSha256,
|
|
167
167
|
extracted_at: a.extractedAt,
|
|
168
168
|
}, now);
|
|
169
|
+
const FILE_SOURCE = "file";
|
|
170
|
+
const SAME_OCCURRENCE_MS = 15 * 60_000;
|
|
171
|
+
/** The account's record of this occurrence under another key: same series, the nearest start within 15 minutes. */
|
|
172
|
+
const sameOccurrence = (database, meeting, seriesKey) => {
|
|
173
|
+
if (!seriesKey || meeting.startedAt === null)
|
|
174
|
+
return undefined;
|
|
175
|
+
const row = database
|
|
176
|
+
.prepare(`SELECT m.id,m.external_id,m.meeting_series_id,
|
|
177
|
+
EXISTS (SELECT 1 FROM meeting_transcripts t WHERE t.meeting_id=m.id AND t.source=?) AND
|
|
178
|
+
NOT EXISTS (SELECT 1 FROM meeting_transcripts t WHERE t.meeting_id=m.id AND t.source!=?) AS from_file
|
|
179
|
+
FROM meetings m JOIN meeting_series s ON s.id=m.meeting_series_id
|
|
180
|
+
WHERE m.account_id=? AND s.account_id=? AND s.external_id=? AND m.deleted_at IS NULL
|
|
181
|
+
AND m.started_at BETWEEN ? AND ? ORDER BY abs(m.started_at-?),m.id LIMIT 1`)
|
|
182
|
+
.get(FILE_SOURCE, FILE_SOURCE, meeting.accountId, meeting.accountId, seriesKey, meeting.startedAt - SAME_OCCURRENCE_MS, meeting.startedAt + SAME_OCCURRENCE_MS, meeting.startedAt);
|
|
183
|
+
return row
|
|
184
|
+
? {
|
|
185
|
+
id: row.id,
|
|
186
|
+
externalId: row.external_id,
|
|
187
|
+
seriesId: row.meeting_series_id,
|
|
188
|
+
fromFile: Number(row.from_file) === 1,
|
|
189
|
+
}
|
|
190
|
+
: undefined;
|
|
191
|
+
};
|
|
192
|
+
/** A file's copy of an occurrence the provider already gave: its transcript only, and only where there is none. */
|
|
193
|
+
const fillGap = (context, input, meetingId) => {
|
|
194
|
+
const { database } = context;
|
|
195
|
+
const held = database
|
|
196
|
+
.prepare("SELECT 1 FROM meeting_transcripts WHERE meeting_id = ? AND superseded_at IS NULL AND deleted_at IS NULL")
|
|
197
|
+
.get(meetingId);
|
|
198
|
+
if (held || !input.transcripts?.length)
|
|
199
|
+
return meetingId;
|
|
200
|
+
const participantIds = (input.participants ?? []).map(({ identity, ...p }) => upsert(database, "meeting_participants", {
|
|
201
|
+
meeting_id: meetingId,
|
|
202
|
+
identity_id: meetingIdentityPk({ ...context, now: () => input.now }, input.meeting.accountId, identity),
|
|
203
|
+
}, { display_name: p.displayName, email: p.email, role: p.role, external_id: p.externalId }, input.now));
|
|
204
|
+
saveTranscripts(database, meetingId, input.transcripts, (position) => (position === null ? null : (participantIds[position] ?? null)), input.now);
|
|
205
|
+
return meetingId;
|
|
206
|
+
};
|
|
169
207
|
const save = (context, input) => {
|
|
170
208
|
const { database } = context;
|
|
171
209
|
const { meeting: { series: seriesInput, ...meeting }, now, } = input;
|
|
172
210
|
if (!Number.isSafeInteger(meeting.accountId) || meeting.accountId <= 0 || !meeting.externalId)
|
|
173
211
|
throw new Error("Invalid meeting key");
|
|
174
|
-
|
|
212
|
+
let existing = database
|
|
175
213
|
.prepare("SELECT id, meeting_series_id FROM meetings WHERE account_id = ? AND external_id = ?")
|
|
176
214
|
.get(meeting.accountId, meeting.externalId);
|
|
215
|
+
const same = existing ? undefined : sameOccurrence(database, meeting, seriesInput?.externalId);
|
|
216
|
+
if (same?.fromFile === false && input.fromFile)
|
|
217
|
+
return fillGap(context, input, Number(same.id));
|
|
218
|
+
if (same && (same.fromFile || input.fromFile)) {
|
|
219
|
+
// A provider record takes over a record made from files; two files of one occurrence share the first one's key.
|
|
220
|
+
if (input.fromFile)
|
|
221
|
+
meeting.externalId = String(same.externalId);
|
|
222
|
+
else
|
|
223
|
+
database
|
|
224
|
+
.prepare("UPDATE meetings SET external_id = ?, updated_at = ? WHERE id = ?")
|
|
225
|
+
.run(meeting.externalId, now, Number(same.id));
|
|
226
|
+
existing = { id: same.id, meeting_series_id: same.seriesId };
|
|
227
|
+
}
|
|
177
228
|
let seriesId = existing ? int(existing.meeting_series_id) : null;
|
|
178
229
|
if (seriesInput) {
|
|
179
230
|
if (!seriesInput.externalId)
|
|
@@ -228,6 +279,10 @@ const save = (context, input) => {
|
|
|
228
279
|
return id;
|
|
229
280
|
};
|
|
230
281
|
saveTranscripts(database, meetingId, input.transcripts ?? [], participantId, now);
|
|
282
|
+
if ((input.transcripts ?? []).some((transcript) => transcript.source !== FILE_SOURCE))
|
|
283
|
+
database
|
|
284
|
+
.prepare("UPDATE meeting_transcripts SET superseded_at = ?, updated_at = ? WHERE meeting_id = ? AND source = ? AND superseded_at IS NULL")
|
|
285
|
+
.run(now, now, meetingId, FILE_SOURCE);
|
|
231
286
|
for (const { senderParticipantPosition, ...line } of input.chat ?? []) {
|
|
232
287
|
const values = {
|
|
233
288
|
sent_at: line.sentAt,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Id, Message, PollVote, Provider, Reactions } from "../../domain/models.js";
|
|
1
|
+
import type { Id, Message, PersonReaction, PollVote, Provider, Reactions } from "../../domain/models.js";
|
|
2
2
|
import type { AccountKey, DeletionScope } from "../types.js";
|
|
3
3
|
import type { StoreContext } from "./open.js";
|
|
4
4
|
export declare const upsertMessage: (context: StoreContext, key: AccountKey, accountKey: number, chatKey: number, message: Message, via: string, seenAt?: number) => void;
|
|
@@ -7,11 +7,19 @@ export declare const upsertMessage: (context: StoreContext, key: AccountKey, acc
|
|
|
7
7
|
* back, and leaves no text — not in the row, the search copy, the edit history, a transcript or a vector.
|
|
8
8
|
*/
|
|
9
9
|
export declare const tombstone: (context: StoreContext, pk: number) => number;
|
|
10
|
-
/** Whether the message is held at all. */
|
|
11
|
-
export declare const saveReactions: (
|
|
10
|
+
/** Whether the message is held at all. Where people's own reactions are held, the totals come from them instead. */
|
|
11
|
+
export declare const saveReactions: (context: StoreContext, chatKey: number, messageId: Id, reactions: Reactions) => boolean;
|
|
12
12
|
/** Answers whether the message is held; a deleted one keeps no star. */
|
|
13
13
|
export declare const saveStar: ({ orm }: StoreContext, chatKey: number, messageId: Id, starred: boolean) => boolean;
|
|
14
14
|
export declare const markDeleted: (context: StoreContext, accountKey: number, messageIds: Id[], chatId: Id | undefined, among?: DeletionScope) => number;
|
|
15
15
|
/** A vote older than the one held for that voter is dropped, and so is any vote in a deleted poll. */
|
|
16
16
|
export declare const saveVote: (context: StoreContext, accountKey: number, provider: Provider, chatKey: number, messageId: Id, { person, answers, votedAt }: PollVote) => void;
|
|
17
17
|
export declare const pollVotes: ({ database }: StoreContext, chatKey: number, messageId: Id) => PollVote[];
|
|
18
|
+
/**
|
|
19
|
+
* The totals of the people's reactions the store holds for a message, or `undefined` when it holds none: then the
|
|
20
|
+
* messenger's own totals stand. A taken-back reaction still counts as held, so all of them taken back is zero.
|
|
21
|
+
*/
|
|
22
|
+
export declare const derivedReactions: ({ database }: StoreContext, chatKey: number, messageId: Id) => Reactions | undefined;
|
|
23
|
+
/** A reaction older than the one held for that person is dropped, and so is any reaction to a deleted message. */
|
|
24
|
+
export declare const saveReaction: (context: StoreContext, accountKey: number, provider: Provider, chatKey: number, messageId: Id, { person, own, reaction, reactedAt }: PersonReaction) => void;
|
|
25
|
+
export declare const writeDerivedReactions: (context: StoreContext, chatKey: number, messageId: Id) => void;
|
|
@@ -174,6 +174,13 @@ export const upsertMessage = (context, key, accountKey, chatKey, message, via, s
|
|
|
174
174
|
if (revived)
|
|
175
175
|
orm.update(messages).set({ deletedAt: null, text: message.text }).where(eq(messages.id, pk)).run();
|
|
176
176
|
}
|
|
177
|
+
const derived = derivedReactions(context, chatKey, message.id);
|
|
178
|
+
if (derived)
|
|
179
|
+
orm
|
|
180
|
+
.update(messages)
|
|
181
|
+
.set({ reactions: JSON.stringify(derived) })
|
|
182
|
+
.where(eq(messages.id, pk))
|
|
183
|
+
.run();
|
|
177
184
|
if (message.counterObservations)
|
|
178
185
|
applyCounterObservations(context, pk, message.counterObservations);
|
|
179
186
|
if (message.starred !== undefined)
|
|
@@ -237,6 +244,9 @@ export const tombstone = (context, pk) => {
|
|
|
237
244
|
context.database
|
|
238
245
|
.prepare("DELETE FROM poll_votes WHERE chat_id=? AND message_external_id=?")
|
|
239
246
|
.run(row.chatId, row.externalId);
|
|
247
|
+
context.database
|
|
248
|
+
.prepare("DELETE FROM message_reactions WHERE chat_id=? AND message_external_id=?")
|
|
249
|
+
.run(row.chatId, row.externalId);
|
|
240
250
|
purgeVectorsOf(context, pk);
|
|
241
251
|
orm
|
|
242
252
|
.delete(messageTranscripts)
|
|
@@ -244,10 +254,10 @@ export const tombstone = (context, pk) => {
|
|
|
244
254
|
.run();
|
|
245
255
|
return 1;
|
|
246
256
|
};
|
|
247
|
-
/** Whether the message is held at all. */
|
|
248
|
-
export const saveReactions = (
|
|
257
|
+
/** Whether the message is held at all. Where people's own reactions are held, the totals come from them instead. */
|
|
258
|
+
export const saveReactions = (context, chatKey, messageId, reactions) => context.orm
|
|
249
259
|
.update(messages)
|
|
250
|
-
.set({ reactions: JSON.stringify(reactions) })
|
|
260
|
+
.set({ reactions: JSON.stringify(derivedReactions(context, chatKey, messageId) ?? reactions) })
|
|
251
261
|
.where(and(eq(messages.chatId, chatKey), eq(messages.externalId, messageId), isNull(messages.deletedAt)))
|
|
252
262
|
.returning({ pk: messages.id })
|
|
253
263
|
.all().length > 0;
|
|
@@ -326,3 +336,64 @@ export const pollVotes = ({ database }, chatKey, messageId) => database
|
|
|
326
336
|
answers: JSON.parse(answers),
|
|
327
337
|
votedAt: new Date(votedAt).toISOString(),
|
|
328
338
|
}));
|
|
339
|
+
const reactionStatements = new WeakMap();
|
|
340
|
+
const reactionStatementsOf = (database) => {
|
|
341
|
+
const held = reactionStatements.get(database);
|
|
342
|
+
if (held)
|
|
343
|
+
return held;
|
|
344
|
+
const statements = {
|
|
345
|
+
gone: database.prepare("SELECT 1 FROM messages WHERE chat_id=? AND external_id=? AND deleted_at IS NOT NULL"),
|
|
346
|
+
upsert: database.prepare(`INSERT INTO message_reactions
|
|
347
|
+
(chat_id, message_external_id, identity_id, own, reaction, reacted_at, created_at, updated_at)
|
|
348
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
349
|
+
ON CONFLICT (chat_id, message_external_id, identity_id) DO UPDATE SET
|
|
350
|
+
own = excluded.own, reaction = excluded.reaction, reacted_at = excluded.reacted_at,
|
|
351
|
+
updated_at = excluded.updated_at
|
|
352
|
+
WHERE excluded.reacted_at >= message_reactions.reacted_at`),
|
|
353
|
+
held: database.prepare("SELECT reaction, own, reacted_at AS reactedAt FROM message_reactions WHERE chat_id=? AND message_external_id=?"),
|
|
354
|
+
total: database.prepare("UPDATE messages SET reactions=? WHERE chat_id=? AND external_id=? AND deleted_at IS NULL"),
|
|
355
|
+
};
|
|
356
|
+
reactionStatements.set(database, statements);
|
|
357
|
+
return statements;
|
|
358
|
+
};
|
|
359
|
+
/**
|
|
360
|
+
* The totals of the people's reactions the store holds for a message, or `undefined` when it holds none: then the
|
|
361
|
+
* messenger's own totals stand. A taken-back reaction still counts as held, so all of them taken back is zero.
|
|
362
|
+
*/
|
|
363
|
+
export const derivedReactions = ({ database }, chatKey, messageId) => {
|
|
364
|
+
const rows = reactionStatementsOf(database).held.all(chatKey, messageId);
|
|
365
|
+
if (rows.length === 0)
|
|
366
|
+
return undefined;
|
|
367
|
+
const counts = new Map();
|
|
368
|
+
let mine = null;
|
|
369
|
+
for (const { reaction, own, reactedAt } of rows) {
|
|
370
|
+
if (reaction === null)
|
|
371
|
+
continue;
|
|
372
|
+
const held = counts.get(reaction);
|
|
373
|
+
counts.set(reaction, { count: (held?.count ?? 0) + 1, first: Math.min(held?.first ?? reactedAt, reactedAt) });
|
|
374
|
+
if (own === 1 && (mine === null || reactedAt > mine.at))
|
|
375
|
+
mine = { reaction, at: reactedAt };
|
|
376
|
+
}
|
|
377
|
+
const sorted = [...counts].sort(([, a], [, b]) => b.count - a.count || a.first - b.first);
|
|
378
|
+
return {
|
|
379
|
+
counts: sorted.map(([reaction, { count }]) => ({ reaction, count })),
|
|
380
|
+
mine: mine?.reaction ?? null,
|
|
381
|
+
total: sorted.reduce((sum, [, { count }]) => sum + count, 0),
|
|
382
|
+
};
|
|
383
|
+
};
|
|
384
|
+
/** A reaction older than the one held for that person is dropped, and so is any reaction to a deleted message. */
|
|
385
|
+
export const saveReaction = (context, accountKey, provider, chatKey, messageId, { person, own, reaction, reactedAt }) => {
|
|
386
|
+
const { database, now } = context;
|
|
387
|
+
const statements = reactionStatementsOf(database);
|
|
388
|
+
if (statements.gone.get(chatKey, messageId))
|
|
389
|
+
return;
|
|
390
|
+
const reactor = identityPk(context, accountKey, provider, person.id, person.name, person.username ? { username: person.username } : {});
|
|
391
|
+
const at = toMs(reactedAt) ?? now();
|
|
392
|
+
statements.upsert.run(chatKey, messageId, reactor, own ? 1 : 0, reaction, at, now(), now());
|
|
393
|
+
writeDerivedReactions(context, chatKey, messageId);
|
|
394
|
+
};
|
|
395
|
+
export const writeDerivedReactions = (context, chatKey, messageId) => {
|
|
396
|
+
const derived = derivedReactions(context, chatKey, messageId);
|
|
397
|
+
if (derived)
|
|
398
|
+
reactionStatementsOf(context.database).total.run(JSON.stringify(derived), chatKey, messageId);
|
|
399
|
+
};
|
|
@@ -554,5 +554,12 @@ export const GENERATED = [
|
|
|
554
554
|
"CREATE TRIGGER chat_revisions_au AFTER UPDATE OF external_id, title, membership_state, scope, searchable, parent_chat_id ON chats BEGIN\n INSERT INTO chat_revisions (chat_id, account_id, field, old_value, new_value, source, created_at)\n SELECT new.id, new.account_id, f.field, f.old_value, f.new_value, s.source,\n coalesce(s.at, CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER))\n FROM (\n SELECT 'external_id' AS field, old.external_id AS old_value, new.external_id AS new_value\n UNION ALL SELECT 'title', old.title, new.title\n UNION ALL SELECT 'membership_state', old.membership_state, new.membership_state\n UNION ALL SELECT 'scope', old.scope, new.scope\n UNION ALL SELECT 'searchable', old.searchable, new.searchable\n UNION ALL SELECT 'parent_chat_id', old.parent_chat_id, new.parent_chat_id\n ) f\n LEFT JOIN chat_revision_source s ON s.id = 1\n WHERE f.old_value IS NOT f.new_value;\nEND;",
|
|
555
555
|
"CREATE TRIGGER chat_revisions_ad AFTER DELETE ON chats BEGIN\n DELETE FROM chat_revisions WHERE chat_id = old.id;\nEND;"
|
|
556
556
|
]
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
"name": "20261021214318_version-26-message-reactions",
|
|
560
|
+
"statements": [
|
|
561
|
+
"CREATE TABLE `message_reactions` (\n\t`id` integer PRIMARY KEY,\n\t`chat_id` integer NOT NULL,\n\t`message_external_id` text NOT NULL,\n\t`identity_id` integer NOT NULL,\n\t`own` integer NOT NULL,\n\t`reaction` text,\n\t`reacted_at` integer NOT NULL,\n\t`created_at` integer NOT NULL,\n\t`updated_at` integer NOT NULL,\n\tCONSTRAINT `fk_message_reactions_chat_id_chats_id_fk` FOREIGN KEY (`chat_id`) REFERENCES `chats`(`id`),\n\tCONSTRAINT `fk_message_reactions_identity_id_identities_id_fk` FOREIGN KEY (`identity_id`) REFERENCES `identities`(`id`),\n\tCONSTRAINT `message_reactions_chat_id_message_external_id_identity_id_unique` UNIQUE(`chat_id`,`message_external_id`,`identity_id`)\n);",
|
|
562
|
+
"CREATE INDEX `message_reactions_by_identity_id` ON `message_reactions` (`identity_id`);"
|
|
563
|
+
]
|
|
557
564
|
}
|
|
558
565
|
];
|
|
@@ -13,6 +13,20 @@ import { assertReadCompatible } from "./read-compatibility.js";
|
|
|
13
13
|
import { stemsState } from "./stems.js";
|
|
14
14
|
import { fillStems, followDefaultStemmers } from "./stems-fill.js";
|
|
15
15
|
export const BACKFILL_ON_OPEN = 5_000;
|
|
16
|
+
const NOT_WRITABLE = new Set(["EACCES", "EPERM", "EROFS"]);
|
|
17
|
+
const createWritable = (file) => {
|
|
18
|
+
try {
|
|
19
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
20
|
+
// SQLite gives its sidecar files the database's permissions.
|
|
21
|
+
writeFileSync(file, "", { flag: "a", mode: 0o600 });
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (!NOT_WRITABLE.has(error.code ?? ""))
|
|
25
|
+
throw error;
|
|
26
|
+
// The errno's own sentence names the path, which holds the owner's home folder.
|
|
27
|
+
throw new CliError("permission_error", "the message store cannot be written from here; run where it is writable, or set MESSAGING_STORE", { reason: "store.not-writable" });
|
|
28
|
+
}
|
|
29
|
+
};
|
|
16
30
|
export const openStoreContext = async ({ path, env, now = Date.now, command, readOnly = false, } = {}) => {
|
|
17
31
|
if (typeof readOnly !== "boolean")
|
|
18
32
|
throw new CliError("validation_error", "readOnly must be a boolean");
|
|
@@ -22,11 +36,8 @@ export const openStoreContext = async ({ path, env, now = Date.now, command, rea
|
|
|
22
36
|
if (!statSync(file, { throwIfNoEntry: false })?.isFile())
|
|
23
37
|
throw new CliError("not_found", "The store does not exist as a file; read-only opening does not create it");
|
|
24
38
|
}
|
|
25
|
-
else
|
|
26
|
-
|
|
27
|
-
// SQLite gives its sidecar files the database's permissions.
|
|
28
|
-
writeFileSync(file, "", { flag: "a", mode: 0o600 });
|
|
29
|
-
}
|
|
39
|
+
else
|
|
40
|
+
createWritable(file);
|
|
30
41
|
const opened = await openSqlite(file, { readOnly });
|
|
31
42
|
const database = opened.database;
|
|
32
43
|
try {
|
|
@@ -4478,6 +4478,161 @@ export declare const chatEventPeople: import("drizzle-orm/sqlite-core").SQLiteTa
|
|
|
4478
4478
|
};
|
|
4479
4479
|
dialect: 'sqlite';
|
|
4480
4480
|
}>;
|
|
4481
|
+
/**
|
|
4482
|
+
* Each person's newest reaction to a message, for a messenger that sends one person's change at a time; the store
|
|
4483
|
+
* derives `messages.reactions` from them. Keyed by chat and external id: a reaction can arrive before its message.
|
|
4484
|
+
*/
|
|
4485
|
+
export declare const messageReactions: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
4486
|
+
name: "message_reactions";
|
|
4487
|
+
schema: undefined;
|
|
4488
|
+
columns: {
|
|
4489
|
+
id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4490
|
+
name: string;
|
|
4491
|
+
tableName: "message_reactions";
|
|
4492
|
+
dataType: "number int53";
|
|
4493
|
+
data: number;
|
|
4494
|
+
driverParam: number;
|
|
4495
|
+
notNull: true;
|
|
4496
|
+
hasDefault: true;
|
|
4497
|
+
isPrimaryKey: true;
|
|
4498
|
+
isAutoincrement: false;
|
|
4499
|
+
hasRuntimeDefault: false;
|
|
4500
|
+
enumValues: undefined;
|
|
4501
|
+
baseColumn: never;
|
|
4502
|
+
identity: undefined;
|
|
4503
|
+
generated: undefined;
|
|
4504
|
+
}, {}>;
|
|
4505
|
+
chatId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4506
|
+
name: string;
|
|
4507
|
+
tableName: "message_reactions";
|
|
4508
|
+
dataType: "number int53";
|
|
4509
|
+
data: number;
|
|
4510
|
+
driverParam: number;
|
|
4511
|
+
notNull: true;
|
|
4512
|
+
hasDefault: false;
|
|
4513
|
+
isPrimaryKey: false;
|
|
4514
|
+
isAutoincrement: false;
|
|
4515
|
+
hasRuntimeDefault: false;
|
|
4516
|
+
enumValues: undefined;
|
|
4517
|
+
baseColumn: never;
|
|
4518
|
+
identity: undefined;
|
|
4519
|
+
generated: undefined;
|
|
4520
|
+
}, {}>;
|
|
4521
|
+
messageExternalId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4522
|
+
name: string;
|
|
4523
|
+
tableName: "message_reactions";
|
|
4524
|
+
dataType: "string";
|
|
4525
|
+
data: string;
|
|
4526
|
+
driverParam: string;
|
|
4527
|
+
notNull: true;
|
|
4528
|
+
hasDefault: false;
|
|
4529
|
+
isPrimaryKey: false;
|
|
4530
|
+
isAutoincrement: false;
|
|
4531
|
+
hasRuntimeDefault: false;
|
|
4532
|
+
enumValues: [string, ...string[]];
|
|
4533
|
+
baseColumn: never;
|
|
4534
|
+
identity: undefined;
|
|
4535
|
+
generated: undefined;
|
|
4536
|
+
}, {}>;
|
|
4537
|
+
identityId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4538
|
+
name: string;
|
|
4539
|
+
tableName: "message_reactions";
|
|
4540
|
+
dataType: "number int53";
|
|
4541
|
+
data: number;
|
|
4542
|
+
driverParam: number;
|
|
4543
|
+
notNull: true;
|
|
4544
|
+
hasDefault: false;
|
|
4545
|
+
isPrimaryKey: false;
|
|
4546
|
+
isAutoincrement: false;
|
|
4547
|
+
hasRuntimeDefault: false;
|
|
4548
|
+
enumValues: undefined;
|
|
4549
|
+
baseColumn: never;
|
|
4550
|
+
identity: undefined;
|
|
4551
|
+
generated: undefined;
|
|
4552
|
+
}, {}>;
|
|
4553
|
+
own: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4554
|
+
name: string;
|
|
4555
|
+
tableName: "message_reactions";
|
|
4556
|
+
dataType: "number int53";
|
|
4557
|
+
data: number;
|
|
4558
|
+
driverParam: number;
|
|
4559
|
+
notNull: true;
|
|
4560
|
+
hasDefault: false;
|
|
4561
|
+
isPrimaryKey: false;
|
|
4562
|
+
isAutoincrement: false;
|
|
4563
|
+
hasRuntimeDefault: false;
|
|
4564
|
+
enumValues: undefined;
|
|
4565
|
+
baseColumn: never;
|
|
4566
|
+
identity: undefined;
|
|
4567
|
+
generated: undefined;
|
|
4568
|
+
}, {}>;
|
|
4569
|
+
reaction: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4570
|
+
name: string;
|
|
4571
|
+
tableName: "message_reactions";
|
|
4572
|
+
dataType: "string";
|
|
4573
|
+
data: string;
|
|
4574
|
+
driverParam: string;
|
|
4575
|
+
notNull: false;
|
|
4576
|
+
hasDefault: false;
|
|
4577
|
+
isPrimaryKey: false;
|
|
4578
|
+
isAutoincrement: false;
|
|
4579
|
+
hasRuntimeDefault: false;
|
|
4580
|
+
enumValues: [string, ...string[]];
|
|
4581
|
+
baseColumn: never;
|
|
4582
|
+
identity: undefined;
|
|
4583
|
+
generated: undefined;
|
|
4584
|
+
}, {}>;
|
|
4585
|
+
reactedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4586
|
+
name: string;
|
|
4587
|
+
tableName: "message_reactions";
|
|
4588
|
+
dataType: "number int53";
|
|
4589
|
+
data: number;
|
|
4590
|
+
driverParam: number;
|
|
4591
|
+
notNull: true;
|
|
4592
|
+
hasDefault: false;
|
|
4593
|
+
isPrimaryKey: false;
|
|
4594
|
+
isAutoincrement: false;
|
|
4595
|
+
hasRuntimeDefault: false;
|
|
4596
|
+
enumValues: undefined;
|
|
4597
|
+
baseColumn: never;
|
|
4598
|
+
identity: undefined;
|
|
4599
|
+
generated: undefined;
|
|
4600
|
+
}, {}>;
|
|
4601
|
+
createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4602
|
+
name: string;
|
|
4603
|
+
tableName: "message_reactions";
|
|
4604
|
+
dataType: "number int53";
|
|
4605
|
+
data: number;
|
|
4606
|
+
driverParam: number;
|
|
4607
|
+
notNull: true;
|
|
4608
|
+
hasDefault: false;
|
|
4609
|
+
isPrimaryKey: false;
|
|
4610
|
+
isAutoincrement: false;
|
|
4611
|
+
hasRuntimeDefault: false;
|
|
4612
|
+
enumValues: undefined;
|
|
4613
|
+
baseColumn: never;
|
|
4614
|
+
identity: undefined;
|
|
4615
|
+
generated: undefined;
|
|
4616
|
+
}, {}>;
|
|
4617
|
+
updatedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4618
|
+
name: string;
|
|
4619
|
+
tableName: "message_reactions";
|
|
4620
|
+
dataType: "number int53";
|
|
4621
|
+
data: number;
|
|
4622
|
+
driverParam: number;
|
|
4623
|
+
notNull: true;
|
|
4624
|
+
hasDefault: false;
|
|
4625
|
+
isPrimaryKey: false;
|
|
4626
|
+
isAutoincrement: false;
|
|
4627
|
+
hasRuntimeDefault: false;
|
|
4628
|
+
enumValues: undefined;
|
|
4629
|
+
baseColumn: never;
|
|
4630
|
+
identity: undefined;
|
|
4631
|
+
generated: undefined;
|
|
4632
|
+
}, {}>;
|
|
4633
|
+
};
|
|
4634
|
+
dialect: 'sqlite';
|
|
4635
|
+
}>;
|
|
4481
4636
|
/** One read of a chat's member list. Retention reads presence at a checkpoint from these, not from stays. */
|
|
4482
4637
|
export declare const memberObservations: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
4483
4638
|
name: "member_observations";
|
|
@@ -511,6 +511,28 @@ export const chatEventPeople = sqliteTable("chat_event_people", {
|
|
|
511
511
|
primaryKey({ columns: [table.chatEventId, table.identityId] }),
|
|
512
512
|
index("chat_event_people_by_identity_id").on(table.identityId),
|
|
513
513
|
]);
|
|
514
|
+
/**
|
|
515
|
+
* Each person's newest reaction to a message, for a messenger that sends one person's change at a time; the store
|
|
516
|
+
* derives `messages.reactions` from them. Keyed by chat and external id: a reaction can arrive before its message.
|
|
517
|
+
*/
|
|
518
|
+
export const messageReactions = sqliteTable("message_reactions", {
|
|
519
|
+
id: integer("id").primaryKey(),
|
|
520
|
+
chatId: integer("chat_id")
|
|
521
|
+
.notNull()
|
|
522
|
+
.references(() => chats.id),
|
|
523
|
+
messageExternalId: text("message_external_id").notNull(),
|
|
524
|
+
identityId: integer("identity_id")
|
|
525
|
+
.notNull()
|
|
526
|
+
.references(() => identities.id),
|
|
527
|
+
own: integer("own").notNull(),
|
|
528
|
+
reaction: text("reaction"),
|
|
529
|
+
reactedAt: integer("reacted_at").notNull(),
|
|
530
|
+
createdAt: integer("created_at").notNull(),
|
|
531
|
+
updatedAt: integer("updated_at").notNull(),
|
|
532
|
+
}, (table) => [
|
|
533
|
+
unique().on(table.chatId, table.messageExternalId, table.identityId),
|
|
534
|
+
index("message_reactions_by_identity_id").on(table.identityId),
|
|
535
|
+
]);
|
|
514
536
|
/** One read of a chat's member list. Retention reads presence at a checkpoint from these, not from stays. */
|
|
515
537
|
export const memberObservations = sqliteTable("member_observations", {
|
|
516
538
|
id: integer("id").primaryKey(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.381.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",
|
|
@@ -173,6 +173,7 @@
|
|
|
173
173
|
"@xmldom/xmldom": "0.9.12",
|
|
174
174
|
"chardet": "2.2.0",
|
|
175
175
|
"defuddle": "0.19.5",
|
|
176
|
+
"dictionary-ru": "3.0.0",
|
|
176
177
|
"echarts": "6.1.0",
|
|
177
178
|
"fflate": "0.8.3",
|
|
178
179
|
"iconv-lite": "0.7.3",
|
|
@@ -202,7 +203,7 @@
|
|
|
202
203
|
"@types/node": "^22.20.4",
|
|
203
204
|
"@types/pngjs": "6.0.5",
|
|
204
205
|
"@vitest/coverage-v8": "^5.0.2",
|
|
205
|
-
"@wirecat/cli-base": "0.
|
|
206
|
+
"@wirecat/cli-base": "0.49.0",
|
|
206
207
|
"commander": "15.0.0",
|
|
207
208
|
"cspell": "10.3.6",
|
|
208
209
|
"drizzle-kit": "1.0.0-rc.4",
|
|
@@ -222,7 +223,7 @@
|
|
|
222
223
|
},
|
|
223
224
|
"peerDependencies": {
|
|
224
225
|
"@napi-rs/canvas": ">=0.1.69 <2",
|
|
225
|
-
"@wirecat/cli-base": ">=0.
|
|
226
|
+
"@wirecat/cli-base": ">=0.49.0 <0.50.0",
|
|
226
227
|
"commander": ">=15"
|
|
227
228
|
},
|
|
228
229
|
"bin": {
|