@wirecat/cli-core 0.500.0 → 0.501.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/calendar/mcp/index.d.ts +12 -12
  3. package/dist/cli/envelope.d.ts +11 -1
  4. package/dist/cli/envelope.js +15 -3
  5. package/dist/cli/index.d.ts +1 -1
  6. package/dist/cli/index.js +1 -1
  7. package/dist/cli/meeting-preparation-answer.d.ts +8 -0
  8. package/dist/cli/meeting-preparation-projects.d.ts +8 -0
  9. package/dist/cli/meeting-preparation-read.d.ts +8 -0
  10. package/dist/cli/meeting-series-inventory-read.d.ts +8 -0
  11. package/dist/cli/messenger/messages-search-command.js +4 -1
  12. package/dist/cli/messenger/search-answers.d.ts +3 -0
  13. package/dist/cli/messenger/search-answers.js +6 -0
  14. package/dist/cli/messenger/search-command.js +4 -1
  15. package/dist/entities/index.d.ts +24 -0
  16. package/dist/entities/meeting-preparation.d.ts +8 -0
  17. package/dist/entities/meeting-series-inventory.d.ts +8 -0
  18. package/dist/mcp/meeting-preparation.d.ts +8 -0
  19. package/dist/mcp/meeting-series-inventory.d.ts +8 -0
  20. package/dist/meetings/mcp/index.d.ts +15 -15
  21. package/dist/meetings/project-evidence-answer.d.ts +8 -0
  22. package/dist/search/total.d.ts +13 -0
  23. package/dist/search/total.js +14 -0
  24. package/dist/services/index.d.ts +2 -1
  25. package/dist/services/index.js +2 -1
  26. package/dist/services/messages-archive-search.js +13 -3
  27. package/dist/services/messages-discovery.d.ts +6 -0
  28. package/dist/services/messages-discovery.js +14 -11
  29. package/dist/services/messages-search.d.ts +5 -0
  30. package/dist/services/messages-search.js +22 -0
  31. package/dist/services/messages.d.ts +3 -0
  32. package/dist/services/notes-search.d.ts +14 -2
  33. package/dist/services/notes-search.js +101 -19
  34. package/dist/store/queries.d.ts +2 -0
  35. package/dist/store/queries.js +1 -0
  36. package/dist/store/sqlite/lucene.d.ts +5 -0
  37. package/dist/store/sqlite/lucene.js +17 -0
  38. package/dist/store/sqlite/note-search.d.ts +18 -0
  39. package/dist/store/sqlite/note-search.js +40 -4
  40. package/package.json +3 -3
@@ -1,4 +1,4 @@
1
- import { CliError } from "@wirecat/cli-base";
1
+ import { CliError, isCliError } from "@wirecat/cli-base";
2
2
  import { dateRange, parseLucene, timezoneOf } from "@wirecat/cli-base/query";
3
3
  import { INTERNAL_NOTES } from "../domain/providers.js";
4
4
  import { normalizeTag } from "../domain/tags.js";
@@ -10,6 +10,7 @@ import { validateFields } from "../search/lucene/registry.js";
10
10
  import { hasStems } from "../search/lucene/resolved.js";
11
11
  import { SEARCH_QUERY } from "../search/lucene/types.js";
12
12
  import { createStemmer, DEFAULT_STEMMERS } from "../search/stem.js";
13
+ import { TOTAL_CAP, totalOf } from "../search/total.js";
13
14
  import { openPictureEmbedderFor } from "./document-pictures.js";
14
15
  import { documentSource } from "./document-source.js";
15
16
  import { plainQueryWords, titleQuery, titleTiers } from "./notes-title-tier.js";
@@ -122,7 +123,17 @@ export const embedEvents = (store, options = {}) => embedChunks(store, store.cal
122
123
  /** Embeds mail's chunks as notes' are embedded: same model and key, so search by meaning reads both. */
123
124
  export const embedMail = (store, options = {}) => embedChunks(store, store.mail, options);
124
125
  /** Notes nearest in meaning to the query, best first; only notes already embedded can be found. */
125
- export const nearestNotes = async (store, query, { model: choice, limit, folderIds, withInternal, source, env, threads, command, embedders, }) => {
126
+ export const nearestNotes = async (store, query, options) => {
127
+ const { key, vector, floor } = await queryVector(store, query, options);
128
+ const nearest = await store.notes.nearest(key, vector, {
129
+ limit: options.limit,
130
+ ...(options.folderIds ? { folderIds: options.folderIds } : {}),
131
+ ...(options.withInternal ? { withInternal: true } : {}),
132
+ ...(options.source ? { source: options.source } : {}),
133
+ });
134
+ return nearest.filter(({ score }) => score > floor);
135
+ };
136
+ const queryVector = async (store, query, { model: choice, folderIds, withInternal, source, env, threads, command, embedders }) => {
126
137
  if (!query.trim())
127
138
  throw new CliError("validation_error", "say what to find in the notes");
128
139
  const { model, directory, key } = localModel(choice, env, command);
@@ -134,14 +145,12 @@ export const nearestNotes = async (store, query, { model: choice, limit, folderI
134
145
  if (!(await store.notes.embedded(key, scope)))
135
146
  throw new CliError("not_found", `nothing searched here has vectors of ${model.id} yet, so only words were matched`);
136
147
  const [vector] = await withLocal(model, directory, threads, (embedder) => embedder.embed([query], "query"), embedders);
137
- const floor = meaningFloor(key);
138
- const nearest = await store.notes.nearest(key, vector, {
139
- limit,
140
- ...(folderIds ? { folderIds } : {}),
141
- ...(withInternal ? { withInternal: true } : {}),
142
- ...(source ? { source } : {}),
143
- });
144
- return nearest.filter(({ score }) => score > floor);
148
+ return { key, vector: vector, floor: meaningFloor(key) };
149
+ };
150
+ /** Every note of `refs` nearer in meaning than the model's floor, best first. */
151
+ const nearestAmong = async (store, query, refs, options) => {
152
+ const { key, vector, floor } = await queryVector(store, query, options);
153
+ return (await store.notes.scoreNotes(key, vector, refs)).filter(({ score }) => score > floor);
145
154
  };
146
155
  const MAX_LINE = 200;
147
156
  const MAX_NOTES_OFFSET = 1000;
@@ -289,6 +298,62 @@ const matching = async (store, refs, root, scope) => {
289
298
  });
290
299
  return new Map(found.items.map(({ ref, note, at }) => [ref, { note, at }]));
291
300
  };
301
+ /**
302
+ * The most notes a filter may hold for meaning to rank all of them, each by its best chunk. Above it, meaning ranks
303
+ * the nearest notes overall and keeps those inside the filter, so a narrow filter over a large store loses some. On
304
+ * the owner's store (1,454 documents, 23,308 chunks) scoring all of them took ~0.5 s against ~0.17 s for the
305
+ * nearest 50 overall, and the cost grows with chunks, so 2,000 notes keeps it under a second at that density.
306
+ */
307
+ export const RANK_WITHIN = 2000;
308
+ /** The notes of a filter, newest first, when there are at most `RANK_WITHIN`; `undefined` when more, or too costly. */
309
+ const filterRefs = async (store, narrowing, scope) => {
310
+ const resolved = await resolvedQuery(store, narrowing, scope.timezone);
311
+ try {
312
+ const found = await store.notes.matchingRefs({
313
+ root: resolved.root,
314
+ limit: RANK_WITHIN,
315
+ ...(scope.folderIds ? { folderIds: scope.folderIds } : {}),
316
+ ...(scope.withInternal ? { withInternal: true } : {}),
317
+ ...(scope.source ? { source: scope.source } : {}),
318
+ ...(resolved.stemmer ? { stemmer: resolved.stemmer } : {}),
319
+ });
320
+ return found.hasMore ? undefined : found.refs;
321
+ }
322
+ catch (error) {
323
+ if (tooLarge(error))
324
+ return undefined;
325
+ throw error;
326
+ }
327
+ };
328
+ const tooLarge = (error) => isCliError(error) && error.details.reason === "query.too-large";
329
+ /**
330
+ * The notes the words find, with those found by meaning: every note the search would list, paged to the end. Never
331
+ * below what the page already showed; a count the store refuses as too costly is that lower bound.
332
+ */
333
+ const notesTotal = async (store, text, { exact, scope, shown, hasMore, }, others, othersComplete) => {
334
+ let words;
335
+ try {
336
+ const { root, stemmer } = await storeQuery(store, text, {
337
+ exact,
338
+ ...(scope.timezone ? { timezone: scope.timezone } : {}),
339
+ });
340
+ words = await store.notes.matchingRefs({
341
+ root,
342
+ limit: TOTAL_CAP,
343
+ ...(scope.folderIds ? { folderIds: scope.folderIds } : {}),
344
+ ...(scope.withInternal ? { withInternal: true } : {}),
345
+ ...(scope.source ? { source: scope.source } : {}),
346
+ ...(stemmer ? { stemmer } : {}),
347
+ });
348
+ }
349
+ catch (error) {
350
+ if (tooLarge(error))
351
+ return totalOf(undefined, shown, hasMore);
352
+ throw error;
353
+ }
354
+ const count = new Set([...words.refs, ...others]).size + (words.hasMore ? 1 : 0);
355
+ return totalOf(count, shown, hasMore, othersComplete);
356
+ };
292
357
  /**
293
358
  * `search notes`: words and stems, and meaning when the local model is there, merged by reciprocal rank;
294
359
  * each hit says which half found it, and the answer lists what the notes found link to. Every field of the query and
@@ -317,24 +382,39 @@ export const searchNotes = async (store, query, { limit = 20, offset = 0, tag, f
317
382
  };
318
383
  let meaningSkipped;
319
384
  let meaning;
385
+ /** Every note found by meaning; `meaningComplete` is false when the nearest overall were cut at the depth. */
386
+ let meaningRefs;
387
+ let meaningComplete = true;
320
388
  if (exact)
321
389
  meaningSkipped = "--exact searches words as written";
322
390
  else if (words) {
391
+ const lane = {
392
+ command,
393
+ ...(env === undefined ? {} : { env }),
394
+ ...(embedders === undefined ? {} : { embedders }),
395
+ ...(folderIds === undefined ? {} : { folderIds }),
396
+ ...(withInternal ? { withInternal: true } : {}),
397
+ ...(source === undefined ? {} : { source }),
398
+ };
399
+ const depth = Math.max(window, CANDIDATES);
323
400
  try {
324
- const nearest = await nearestNotes(store, words, {
325
- limit: Math.max(window, CANDIDATES),
326
- command,
327
- ...(env === undefined ? {} : { env }),
328
- ...(embedders === undefined ? {} : { embedders }),
329
- ...(folderIds === undefined ? {} : { folderIds }),
330
- ...(withInternal ? { withInternal: true } : {}),
331
- ...(source === undefined ? {} : { source }),
332
- });
401
+ const within = narrowing === undefined ? undefined : await filterRefs(store, narrowing, scope);
402
+ let nearest;
403
+ if (within === undefined) {
404
+ nearest = await nearestNotes(store, words, { ...lane, limit: depth });
405
+ meaningComplete = nearest.length < depth;
406
+ }
407
+ else {
408
+ const ranked = within.length ? await nearestAmong(store, words, within, lane) : [];
409
+ meaningRefs = new Set(ranked.map(({ ref }) => ref));
410
+ nearest = ranked.slice(0, depth);
411
+ }
333
412
  const allowed = await matching(store, nearest.map(({ ref }) => ref), narrowing, scope);
334
413
  meaning = nearest.flatMap(({ ref }) => {
335
414
  const found = allowed.get(ref);
336
415
  return found ? [{ ref, ...found }] : [];
337
416
  });
417
+ meaningRefs ??= new Set(meaning.map(({ ref }) => ref));
338
418
  }
339
419
  catch (error) {
340
420
  if (!(error instanceof CliError && error.code === "not_found"))
@@ -378,6 +458,7 @@ export const searchNotes = async (store, query, { limit = 20, offset = 0, tag, f
378
458
  ];
379
459
  const page = slice ? ordered : ordered.slice(offset, window);
380
460
  const hasMore = slice ? matched.hasMore : ordered.length > window || matched.hasMore || titled.hasMore;
461
+ const total = await notesTotal(store, text, { exact, scope, shown: offset + page.length, hasMore }, [...tier.map(({ ref }) => ref), ...(meaningRefs ?? [])], meaningComplete);
381
462
  const hits = [];
382
463
  const counts = new Map();
383
464
  const accounts = await store.accounts.storedAccounts();
@@ -441,6 +522,7 @@ export const searchNotes = async (store, query, { limit = 20, offset = 0, tag, f
441
522
  by: meaning === undefined ? "words" : "words and meaning",
442
523
  ...(meaningSkipped === undefined ? {} : { meaning_skipped: meaningSkipped }),
443
524
  hits,
525
+ total,
444
526
  has_more: hasMore,
445
527
  ...(hasMore && window <= MAX_NOTES_OFFSET ? { next_offset: window } : {}),
446
528
  linked: [...counts.values()].sort((a, b) => b.notes - a.notes || (a.name ?? a.ref ?? "").localeCompare(b.name ?? b.ref ?? "")),
@@ -13,6 +13,8 @@ export interface QueriesStore {
13
13
  matchQuery(execution: QueryExecution): Promise<Page<ScoredHit>>;
14
14
  /** The same matches as `matchQuery`, each counted once, grouped by chat, sender or quarter hour. */
15
15
  countQuery(execution: QueryExecution, by: QueryGrouping): Promise<QueryGroup[]>;
16
+ /** How many messages match, counted no further than `cap + 1`; `undefined` when only a full search could tell. */
17
+ countMatches(execution: QueryExecution, cap: number): Promise<number | undefined>;
16
18
  conversationEligibility(execution: QueryExecution): Promise<ConversationEligibility>;
17
19
  rankQuery(execution: QueryExecution, request: RankingRequest): Promise<RankedStoreFound>;
18
20
  rankingEvidence(execution: QueryExecution, request: RankingEvidenceRequest): Promise<RankedEvidence>;
@@ -13,6 +13,7 @@ export const queriesStoreOver = (context, stemmerFor) => {
13
13
  return {
14
14
  matchQuery: async (execution) => lucene.matchQuery(context, mailIndexed(execution)),
15
15
  countQuery: async (execution, by) => lucene.countQuery(context, mailIndexed(execution), by),
16
+ countMatches: async (execution, cap) => lucene.countMatches(context, mailIndexed(execution), cap),
16
17
  conversationEligibility: async (execution) => conversationEligibility(context, execution),
17
18
  rankQuery: async (execution, request) => rankQuery(context, execution, request),
18
19
  rankingEvidence: async (execution, request) => rankingEvidence(context, execution, request),
@@ -18,6 +18,11 @@ export declare const matchQuery: (context: StoreContext, execution: QueryExecuti
18
18
  export declare const countQuery: (context: StoreContext, execution: QueryExecution, by: QueryGrouping) => Promise<QueryGroup[]>;
19
19
  /** All matching message keys, without materializing message bodies or imposing a hit-page limit. */
20
20
  export declare const queryMessagePks: (context: StoreContext, execution: QueryExecution) => Promise<number[]>;
21
+ /**
22
+ * How many messages match, counting no further than `cap + 1`: one SQL count, unranked. `undefined` when a leaf is
23
+ * checked row by row (a preset, a pattern), where a count would cost as much as the search itself.
24
+ */
25
+ export declare const countMatches: (context: StoreContext, execution: QueryExecution, cap: number) => number | undefined;
21
26
  interface QuerySelection {
22
27
  sql: string;
23
28
  params: SqlValue[];
@@ -48,6 +48,23 @@ export const matchQuery = (context, execution) => runQuery(context, execution);
48
48
  export const countQuery = (context, execution, by) => runQuery(context, execution, by);
49
49
  /** All matching message keys, without materializing message bodies or imposing a hit-page limit. */
50
50
  export const queryMessagePks = (context, execution) => runQuery(context, execution, "pks");
51
+ /**
52
+ * How many messages match, counting no further than `cap + 1`: one SQL count, unranked. `undefined` when a leaf is
53
+ * checked row by row (a preset, a pattern), where a count would cost as much as the search itself.
54
+ */
55
+ export const countMatches = (context, execution, cap) => {
56
+ const { database, check, where, from, withRanking, ranked, leaves } = compileQuery(context, {
57
+ ...execution,
58
+ newest: true,
59
+ });
60
+ if (leaves.some(({ test }) => test))
61
+ return undefined;
62
+ const row = database
63
+ .prepare(`SELECT count(*) AS n FROM (${withRanking}SELECT m.id ${from} WHERE ${where.sql} LIMIT ?)`)
64
+ .get(...ranked.params, ...where.params, cap + 1);
65
+ check();
66
+ return Number(row?.n ?? 0);
67
+ };
51
68
  const compileQuery = (context, execution, boundedAttachments = false) => {
52
69
  const { database } = context;
53
70
  const corpus = execution.corpus === "mail" ? MAIL : MESSAGES;
@@ -79,6 +79,11 @@ export declare const searchNotes: (context: StoreContext, query: NoteQuery) => {
79
79
  items: NoteHit[];
80
80
  hasMore: boolean;
81
81
  };
82
+ /** The refs of the newest `limit` notes matching a query, without reading the notes themselves. */
83
+ export declare const matchingNoteRefs: (context: StoreContext, query: Omit<NoteQuery, "offset" | "newest">) => {
84
+ refs: string[];
85
+ hasMore: boolean;
86
+ };
82
87
  export declare const searchCorpus: (context: StoreContext, query: CorpusQuery, corpus: Corpus) => CorpusRow[];
83
88
  export interface NoteChunkToEmbed {
84
89
  hash: string;
@@ -101,6 +106,11 @@ export interface NoteSearch extends DocumentPictureStore {
101
106
  items: NoteHit[];
102
107
  hasMore: boolean;
103
108
  }>;
109
+ /** The refs of the newest `limit` notes matching the query; `hasMore` when more match. */
110
+ matchingRefs(query: Omit<NoteQuery, "offset" | "newest">): Promise<{
111
+ refs: string[];
112
+ hasMore: boolean;
113
+ }>;
104
114
  indexState(): Promise<NoteIndexState | undefined>;
105
115
  /** Live notes' chunks with no vector of `model`, by hash from `after`, each with its text cut again. */
106
116
  chunksToEmbed(model: string, options: {
@@ -120,6 +130,14 @@ export interface NoteSearch extends DocumentPictureStore {
120
130
  withInternal?: boolean;
121
131
  source?: Note["source"];
122
132
  }): Promise<NearestNote[]>;
133
+ /**
134
+ * Each of `refs` scored by its best chunk, best first, every chunk compared: a filter's own notes ranked, not the
135
+ * nearest overall. A note with no vector of `model` is left out; the notes themselves are not read.
136
+ */
137
+ scoreNotes(model: string, query: Float32Array, refs: string[]): Promise<{
138
+ ref: string;
139
+ score: number;
140
+ }[]>;
123
141
  }
124
142
  export declare const noteSearchOver: (context: StoreContext) => NoteSearch;
125
143
  export {};
@@ -63,9 +63,7 @@ const CORPORA = [
63
63
  tagged: (tag) => bound("n.id IN (SELECT g.taggable_id FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id WHERE t.name = ? AND g.taggable_type = 'note')", tag),
64
64
  },
65
65
  ];
66
- /** Notes matching a parsed query, by the same language as messages: files and written notes, merged. */
67
- export const searchNotes = (context, query) => {
68
- const offset = query.offset ?? 0;
66
+ const matchedRows = (context, query) => {
69
67
  const chosen = CORPORA.filter(({ type }) => (query.source === undefined || (query.source === "file") === (type === "document")) &&
70
68
  (!query.folderIds?.length || query.withInternal === true || type === "document"));
71
69
  const idsOf = (type) => query.refs?.flatMap((ref) => (ref.startsWith(`${type}:`) ? [Number(ref.slice(type.length + 1))] : []));
@@ -83,6 +81,12 @@ export const searchNotes = (context, query) => {
83
81
  (a.relevance ?? 0) - (b.relevance ?? 0) ||
84
82
  (b.at ?? 0) - (a.at ?? 0) ||
85
83
  a.id - b.id);
84
+ return rows;
85
+ };
86
+ /** Notes matching a parsed query, by the same language as messages: files and written notes, merged. */
87
+ export const searchNotes = (context, query) => {
88
+ const offset = query.offset ?? 0;
89
+ const rows = matchedRows(context, query);
86
90
  const read = {
87
91
  document: context.database.prepare("SELECT * FROM documents WHERE id = ?"),
88
92
  note: context.database.prepare("SELECT * FROM notes WHERE id = ?"),
@@ -102,6 +106,11 @@ export const searchNotes = (context, query) => {
102
106
  hasMore: rows.length > offset + query.limit,
103
107
  };
104
108
  };
109
+ /** The refs of the newest `limit` notes matching a query, without reading the notes themselves. */
110
+ export const matchingNoteRefs = (context, query) => {
111
+ const rows = matchedRows(context, { ...query, newest: true });
112
+ return { refs: rows.slice(0, query.limit).map(({ type, id }) => `${type}:${id}`), hasMore: rows.length > query.limit };
113
+ };
105
114
  export const searchCorpus = (context, query, corpus) => {
106
115
  const { database } = context;
107
116
  const wordMatch = (match) => ({
@@ -282,7 +291,7 @@ export const searchCorpus = (context, query, corpus) => {
282
291
  : "";
283
292
  const window = tests.size ? QUERY_LIMITS.candidates + 1 : query.limit + offset + 1;
284
293
  const rows = database
285
- .prepare(`${ctes.length ? `WITH ${ctes.join(", ")} ` : ""}SELECT n.*, ${ranking ? "f.rank" : "NULL"} AS relevance${exactTier ? ", n.id IN (SELECT id FROM x) AS exact" : ""}${projection}
294
+ .prepare(`${ctes.length ? `WITH ${ctes.join(", ")} ` : ""}SELECT ${tests.size ? "n.*" : "n.id, n.at"}, ${ranking ? "f.rank" : "NULL"} AS relevance${exactTier ? ", n.id IN (SELECT id FROM x) AS exact" : ""}${projection}
286
295
  FROM ${corpus.view} n${ranking ? " LEFT JOIN f ON f.id = n.id" : ""} WHERE ${where.sql} ORDER BY ${order} LIMIT ?`)
287
296
  .all(...cteParams, ...leaves.flatMap(([, fragment]) => (tests.size ? fragment.params : [])), ...where.params, window);
288
297
  if (tests.size && rows.length > QUERY_LIMITS.candidates)
@@ -346,6 +355,10 @@ export const noteSearchOver = (context) => {
346
355
  drain();
347
356
  return searchNotes(context, query);
348
357
  },
358
+ matchingRefs: async (query) => {
359
+ drain();
360
+ return matchingNoteRefs(context, query);
361
+ },
349
362
  indexState: async () => noteIndexState(database),
350
363
  chunksToEmbed: async (model, { after, limit }) => {
351
364
  drain();
@@ -456,5 +469,28 @@ export const noteSearchOver = (context) => {
456
469
  return note ? [{ ref: note.ref, note, score, range: { start, end } }] : [];
457
470
  });
458
471
  },
472
+ scoreNotes: async (model, query, refs) => {
473
+ drain();
474
+ const scope = live(undefined, undefined, true);
475
+ const owned = database.prepare(`SELECT k.chunkable_type AS type, k.chunkable_id AS owner, v.vector
476
+ FROM chunks k JOIN embeddings v ON v.model = ? AND v.content_hash = k.content_hash
477
+ WHERE k.chunkable_type = ? AND k.chunkable_id IN (SELECT value FROM json_each(?))${scope.sql}`);
478
+ const best = new Map();
479
+ for (const type of ["document", "note"]) {
480
+ const ids = refs.flatMap((ref) => (ref.startsWith(`${type}:`) ? [Number(ref.slice(type.length + 1))] : []));
481
+ if (!ids.length)
482
+ continue;
483
+ for (const row of owned.all(model, type, JSON.stringify(ids), ...scope.params)) {
484
+ const key = `${row.type}:${row.owner}`;
485
+ const score = dot(query, row.vector);
486
+ if (score > (best.get(key) ?? Number.NEGATIVE_INFINITY))
487
+ best.set(key, score);
488
+ }
489
+ }
490
+ const order = new Map(refs.map((ref, index) => [ref, index]));
491
+ return [...best]
492
+ .map(([ref, score]) => ({ ref, score }))
493
+ .sort((a, b) => b.score - a.score || (order.get(a.ref) ?? 0) - (order.get(b.ref) ?? 0));
494
+ },
459
495
  };
460
496
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.500.0",
3
+ "version": "0.501.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",
@@ -247,7 +247,7 @@
247
247
  "@types/node": "^22.20.5",
248
248
  "@types/pngjs": "6.0.5",
249
249
  "@vitest/coverage-v8": "^5.0.5",
250
- "@wirecat/cli-base": "0.65.5",
250
+ "@wirecat/cli-base": "0.67.0",
251
251
  "@wirecat/cli-core-legacy-tags": "npm:@wirecat/cli-core@0.414.0",
252
252
  "commander": "15.0.0",
253
253
  "cspell": "10.4.0",
@@ -267,7 +267,7 @@
267
267
  },
268
268
  "peerDependencies": {
269
269
  "@napi-rs/canvas": ">=0.1.69 <2",
270
- "@wirecat/cli-base": ">=0.65.0 <0.66.0",
270
+ "@wirecat/cli-base": ">=0.67.0 <0.68.0",
271
271
  "commander": ">=15"
272
272
  },
273
273
  "bin": {