@wirecat/cli-core 0.499.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 (59) hide show
  1. package/CHANGELOG.md +48 -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/domain/knowledge.d.ts +1 -1
  16. package/dist/domain/knowledge.js +1 -1
  17. package/dist/entities/fact.d.ts +15 -1
  18. package/dist/entities/fact.js +6 -1
  19. package/dist/entities/fields.d.ts +11 -0
  20. package/dist/entities/fields.js +11 -0
  21. package/dist/entities/index.d.ts +49 -1
  22. package/dist/entities/meeting-preparation.d.ts +8 -0
  23. package/dist/entities/meeting-series-inventory.d.ts +8 -0
  24. package/dist/entities/memory.d.ts +14 -0
  25. package/dist/entities/memory.js +6 -1
  26. package/dist/mcp/meeting-preparation.d.ts +8 -0
  27. package/dist/mcp/meeting-series-inventory.d.ts +8 -0
  28. package/dist/meetings/mcp/index.d.ts +15 -15
  29. package/dist/meetings/project-evidence-answer.d.ts +8 -0
  30. package/dist/search/total.d.ts +13 -0
  31. package/dist/search/total.js +14 -0
  32. package/dist/services/index.d.ts +2 -1
  33. package/dist/services/index.js +2 -1
  34. package/dist/services/messages-archive-search.js +13 -3
  35. package/dist/services/messages-discovery.d.ts +6 -0
  36. package/dist/services/messages-discovery.js +14 -11
  37. package/dist/services/messages-search.d.ts +5 -0
  38. package/dist/services/messages-search.js +22 -0
  39. package/dist/services/messages.d.ts +3 -0
  40. package/dist/services/notes-search.d.ts +14 -2
  41. package/dist/services/notes-search.js +101 -19
  42. package/dist/store/index.d.ts +1 -0
  43. package/dist/store/project-transfer/request.d.ts +1 -1
  44. package/dist/store/project-transfer/schema.d.ts +2 -2
  45. package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
  46. package/dist/store/queries.d.ts +2 -0
  47. package/dist/store/queries.js +1 -0
  48. package/dist/store/sqlite/evidence-sources.d.ts +23 -0
  49. package/dist/store/sqlite/evidence-sources.js +59 -0
  50. package/dist/store/sqlite/facts.d.ts +16 -0
  51. package/dist/store/sqlite/facts.js +39 -5
  52. package/dist/store/sqlite/lucene.d.ts +5 -0
  53. package/dist/store/sqlite/lucene.js +17 -0
  54. package/dist/store/sqlite/memories.d.ts +28 -0
  55. package/dist/store/sqlite/memories.js +62 -3
  56. package/dist/store/sqlite/note-search.d.ts +18 -0
  57. package/dist/store/sqlite/note-search.js +40 -4
  58. package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
  59. package/package.json +3 -3
@@ -1,5 +1,6 @@
1
1
  import { MEMORY_KINDS, MEMORY_SCOPES, type MEMORY_STATUSES } from "../../domain/knowledge.js";
2
2
  import { type Actor } from "./actors.js";
3
+ import { type EvidenceSource } from "./evidence-sources.js";
3
4
  import { type NoteIndexState } from "./note-index.js";
4
5
  import type { StoreContext } from "./open.js";
5
6
  /** Who wrote it: the owner, or an agent by its bot handle and the model it ran on. */
@@ -14,6 +15,10 @@ export interface Memory {
14
15
  body: string;
15
16
  /** What it is about, as a reference; `null` for a general fact. */
16
17
  subject: string | null;
18
+ /** What the subject is called; `null` for the owner, who has no name in the store, and for a nameless row. */
19
+ subjectName: string | null;
20
+ /** The subject is the owner. */
21
+ subjectIsOwner: boolean;
17
22
  /** `name` is a bot's handle; `null` for a person. */
18
23
  author: {
19
24
  type: Actor["type"];
@@ -59,6 +64,17 @@ export interface MemoriesStore {
59
64
  confirm(reference: string): Promise<Memory>;
60
65
  /** Its evidence no longer holds. */
61
66
  markStale(reference: string): Promise<Memory>;
67
+ /**
68
+ * Erases the memory: its row, its text in the search and meaning indexes, and every link from or to it. A memory
69
+ * that replaced it no longer names it. A backup made before keeps it.
70
+ */
71
+ remove(reference: string): Promise<{
72
+ ref: string;
73
+ removed: true;
74
+ links: number;
75
+ }>;
76
+ /** Each reference it rests on, read back; one deleted since is `missing`. */
77
+ sources(reference: string): Promise<EvidenceSource[]>;
62
78
  /** Indexes what was written since, then finds memories holding every word or a longer one it starts, best first. */
63
79
  search(text: string, options?: {
64
80
  subject?: string;
@@ -82,5 +98,17 @@ export declare const evidenceOf: (context: StoreContext, from: {
82
98
  type: string;
83
99
  id: number;
84
100
  }) => string[];
101
+ /** What a memory's or fact's subject is called, for a reader who should not have to look the reference up. */
102
+ export declare const subjectOf: (database: StoreContext["database"], row: Row) => Pick<Memory, "subject" | "subjectName" | "subjectIsOwner">;
103
+ /** Counts every link from or to a row and drops the ones its delete trigger does not: those pointing at it. */
104
+ export declare const dropLinksOf: (database: StoreContext["database"], thing: {
105
+ type: string;
106
+ id: number;
107
+ }) => number;
108
+ /**
109
+ * Runs a delete so the text does not linger in the file: freed pages are zeroed, the words indexes merged so no
110
+ * segment still holds the erased words, and the write-ahead log emptied when no reader holds it.
111
+ */
112
+ export declare const erase: <T>(database: StoreContext["database"], work: () => T) => T;
85
113
  export declare const memoriesStoreOver: (context: StoreContext) => MemoriesStore;
86
114
  export {};
@@ -2,6 +2,7 @@ import { CliError } from "@wirecat/cli-base";
2
2
  import { MEMORY_KINDS, MEMORY_SCOPES } from "../../domain/knowledge.js";
3
3
  import { normalize } from "../normalize.js";
4
4
  import { botNamed, ownerPerson } from "./actors.js";
5
+ import { evidenceSourcesOf } from "./evidence-sources.js";
5
6
  import { linkKind } from "./link-kinds.js";
6
7
  import { CORPORA, corpusIndexState, drainCorpus } from "./note-index.js";
7
8
  import { requiredThing } from "./notes.js";
@@ -42,6 +43,55 @@ export const evidenceOf = (context, from) => context.database
42
43
  .prepare(`SELECT to_type, to_id FROM links WHERE from_type = ? AND from_id = ? AND kind = ${linkKind("evidence")} AND to_id IS NOT NULL ORDER BY id`)
43
44
  .all(from.type, from.id)
44
45
  .flatMap((row) => referenceOfThing(context.database, storedThing(row.to_type, row.to_id)) ?? []);
46
+ const SUBJECT_NAMES = {
47
+ person: "SELECT p.owner, coalesce(p.name, (SELECT a.name FROM aliases a WHERE a.aliasable_type = 'person' " +
48
+ "AND a.aliasable_id = p.id ORDER BY a.display DESC, a.id LIMIT 1), (SELECT i.name FROM identity_links l " +
49
+ "JOIN identities i ON i.id = l.identity_id WHERE l.person_id = p.id AND i.name IS NOT NULL ORDER BY i.id LIMIT 1)) " +
50
+ "AS name FROM persons p WHERE p.id = ?",
51
+ project: "SELECT 0 AS owner, name FROM projects WHERE id = ?",
52
+ organization: "SELECT 0 AS owner, name FROM organizations WHERE id = ?",
53
+ };
54
+ /** What a memory's or fact's subject is called, for a reader who should not have to look the reference up. */
55
+ export const subjectOf = (database, row) => {
56
+ if (row.subject_type == null)
57
+ return { subject: null, subjectName: null, subjectIsOwner: false };
58
+ const thing = storedThing(row.subject_type, row.subject_id);
59
+ const read = SUBJECT_NAMES[thing.type];
60
+ const named = read === undefined ? undefined : database.prepare(read).get(thing.id);
61
+ return {
62
+ subject: referenceOfThing(database, thing) ?? null,
63
+ subjectName: named?.name == null ? null : String(named.name),
64
+ subjectIsOwner: Number(named?.owner) === 1,
65
+ };
66
+ };
67
+ /** Counts every link from or to a row and drops the ones its delete trigger does not: those pointing at it. */
68
+ export const dropLinksOf = (database, thing) => {
69
+ const count = Number(database
70
+ .prepare("SELECT count(*) AS n FROM links WHERE (from_type = ? AND from_id = ?) OR (to_type = ? AND to_id = ?)")
71
+ .get(thing.type, thing.id, thing.type, thing.id)?.n);
72
+ database.prepare("DELETE FROM links WHERE to_type = ? AND to_id = ?").run(thing.type, thing.id);
73
+ return count;
74
+ };
75
+ /**
76
+ * Runs a delete so the text does not linger in the file: freed pages are zeroed, the words indexes merged so no
77
+ * segment still holds the erased words, and the write-ahead log emptied when no reader holds it.
78
+ */
79
+ export const erase = (database, work) => {
80
+ database.exec("PRAGMA secure_delete = ON");
81
+ try {
82
+ const done = write(database, () => {
83
+ const result = work();
84
+ for (const index of ["memory_words", "memory_stems"])
85
+ database.prepare(`INSERT INTO ${index} (${index}) VALUES ('optimize')`).run();
86
+ return result;
87
+ });
88
+ database.exec("PRAGMA wal_checkpoint(TRUNCATE)");
89
+ return done;
90
+ }
91
+ finally {
92
+ database.exec("PRAGMA secure_delete = OFF");
93
+ }
94
+ };
45
95
  export const memoriesStoreOver = (context) => {
46
96
  const { database, now } = context;
47
97
  const stemmerFor = stemmerCache();
@@ -50,9 +100,7 @@ export const memoriesStoreOver = (context) => {
50
100
  ref: `memory:${row.id}`,
51
101
  kind: row.kind,
52
102
  body: String(row.body),
53
- subject: row.subject_type == null
54
- ? null
55
- : (referenceOfThing(database, storedThing(row.subject_type, row.subject_id)) ?? null),
103
+ ...subjectOf(database, row),
56
104
  author: authorOf(row),
57
105
  model: row.model == null ? null : String(row.model),
58
106
  confidence: row.confidence == null ? null : Number(row.confidence),
@@ -127,6 +175,17 @@ export const memoriesStoreOver = (context) => {
127
175
  },
128
176
  confirm: async (reference) => setStatus(reference, "confirmed", true),
129
177
  markStale: async (reference) => setStatus(reference, "stale", false),
178
+ remove: async (reference) => {
179
+ const id = Number(rowOf(reference).id);
180
+ const links = erase(database, () => {
181
+ database.prepare("UPDATE memories SET supersedes_id = NULL WHERE supersedes_id = ?").run(id);
182
+ const count = dropLinksOf(database, { type: "memory", id });
183
+ database.prepare("DELETE FROM memories WHERE id = ?").run(id);
184
+ return count;
185
+ });
186
+ return { ref: `memory:${id}`, removed: true, links };
187
+ },
188
+ sources: async (reference) => evidenceSourcesOf(database, { type: "memory", id: Number(rowOf(reference).id) }),
130
189
  search: async (text, { subject: subjectRef, scope, limit = 20 } = {}) => {
131
190
  const subject = subjectRef === undefined ? undefined : requiredThing(database, subjectRef);
132
191
  drainCorpus(database, CORPORA.memory, stemmerFor);
@@ -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
  };
@@ -7,7 +7,7 @@ export declare const projectAnnotationSnapshot: (database: CacheDatabase, projec
7
7
  ref: string;
8
8
  type: "decision" | "fact" | "promise" | "question" | "statement";
9
9
  statement: string;
10
- status: "accepted" | "proposed" | "reversed" | "superseded";
10
+ status: "accepted" | "proposed" | "rejected" | "reversed" | "superseded";
11
11
  subject: string | null;
12
12
  occurred_at: number | null;
13
13
  supersedes: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.499.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": {