@wirecat/cli-core 0.452.0 → 0.454.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 (54) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/cli/mcp-server-command.js +5 -1
  3. package/dist/cli/messenger/search-answers.d.ts +1 -1
  4. package/dist/cli/messenger/search-answers.js +1 -1
  5. package/dist/cli/messenger/search-command.js +6 -3
  6. package/dist/entities/contact-context.d.ts +42 -0
  7. package/dist/entities/contact-context.js +11 -0
  8. package/dist/entities/hit.d.ts +3 -3
  9. package/dist/entities/hit.js +3 -3
  10. package/dist/entities/index.d.ts +61 -21
  11. package/dist/entities/task-results.d.ts +20 -20
  12. package/dist/entities/task-results.js +3 -15
  13. package/dist/mcp/http/activity.d.ts +24 -0
  14. package/dist/mcp/http/activity.js +86 -0
  15. package/dist/mcp/http/serve.js +14 -0
  16. package/dist/mcp/http/state.d.ts +5 -0
  17. package/dist/mcp/http/state.js +14 -4
  18. package/dist/meetings/event-store.d.ts +23 -0
  19. package/dist/meetings/index.d.ts +1 -1
  20. package/dist/search/query.js +1 -3
  21. package/dist/services/events-search.d.ts +67 -0
  22. package/dist/services/events-search.js +131 -0
  23. package/dist/services/index.d.ts +3 -2
  24. package/dist/services/index.js +2 -1
  25. package/dist/services/notes-search.d.ts +25 -1
  26. package/dist/services/notes-search.js +24 -11
  27. package/dist/services/person-context.d.ts +20 -1
  28. package/dist/services/person-context.js +46 -1
  29. package/dist/services/search-all-meetings.d.ts +1 -1
  30. package/dist/services/search-all-meetings.js +7 -2
  31. package/dist/services/search-all.d.ts +12 -7
  32. package/dist/services/search-all.js +39 -4
  33. package/dist/store/index.d.ts +1 -0
  34. package/dist/store/index.js +1 -0
  35. package/dist/store/project-transfer/request.d.ts +1 -1
  36. package/dist/store/project-transfer/schema.d.ts +2 -2
  37. package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
  38. package/dist/store/sqlite/calendar.js +85 -11
  39. package/dist/store/sqlite/event-search.d.ts +66 -0
  40. package/dist/store/sqlite/event-search.js +163 -0
  41. package/dist/store/sqlite/files.d.ts +4 -0
  42. package/dist/store/sqlite/files.js +28 -0
  43. package/dist/store/sqlite/link-kinds.d.ts +5 -1
  44. package/dist/store/sqlite/link-kinds.js +5 -0
  45. package/dist/store/sqlite/note-index.d.ts +17 -1
  46. package/dist/store/sqlite/note-index.js +32 -8
  47. package/dist/store/sqlite/note-search.d.ts +43 -0
  48. package/dist/store/sqlite/note-search.js +24 -24
  49. package/dist/store/sqlite/notes.d.ts +4 -5
  50. package/dist/store/sqlite/notes.js +2 -2
  51. package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
  52. package/dist/store/store.d.ts +2 -1
  53. package/dist/store/store.js +2 -1
  54. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import type { ResolvedNode } from "../../search/lucene/resolved.js";
2
2
  import type { Stemmer } from "../../search/stem.js";
3
+ import type { SqlValue } from "../driver.js";
3
4
  import { type DocumentPictureStore } from "./document-pictures.js";
4
5
  import { type NoteIndexState } from "./note-index.js";
5
6
  import { type Note } from "./notes.js";
@@ -28,11 +29,52 @@ export interface NoteHit {
28
29
  }
29
30
  /** The fields a note has. A message-only field (`from:`, `chat:`, …) is refused rather than matching nothing. */
30
31
  export declare const NOTE_FIELDS: readonly ["text", "exact", "body", "tag", "date", "in", "has", "title", "site", "lang", "author", "keyword", "source"];
32
+ interface Fragment {
33
+ sql: string;
34
+ params: SqlValue[];
35
+ /** False when the SQL only narrows the candidates and a JS test decides. */
36
+ exact: boolean;
37
+ fts?: string;
38
+ stems?: string;
39
+ }
40
+ /**
41
+ * Files in notes folders, the notes the owner wrote and calendar events: one query language, each in its own table
42
+ * and index.
43
+ */
44
+ export interface Corpus {
45
+ type: "document" | "note" | "event";
46
+ /** What a hit is called in an error and in `in:` (`in:notes`, `in:events`). */
47
+ noun: "note" | "event";
48
+ fields: readonly string[];
49
+ /** The rows as `n`, with the columns the query reads under one set of names; `at` is the time `date:` reads. */
50
+ view: string;
51
+ words: string;
52
+ stems: string;
53
+ /** The rows a search may return at all, besides `deleted_at`. */
54
+ live?: string;
55
+ /** The rows of these accounts, as a JSON list of ids. */
56
+ inAccounts?: (ids: string) => Fragment;
57
+ /** The tagged rows; a document is also labelled through its folder or a subfolder of it. */
58
+ tagged?: (tag: string) => Fragment;
59
+ }
60
+ export interface CorpusRow {
61
+ type: Corpus["type"];
62
+ id: number;
63
+ /** The time `date:` reads: a note's last change, an event's start. */
64
+ at: number | null;
65
+ relevance: number | null;
66
+ exact?: boolean;
67
+ }
68
+ /** What one corpus is searched by; `accountIds` narrows a corpus that has accounts. */
69
+ export type CorpusQuery = Pick<NoteQuery, "root" | "limit" | "offset" | "newest" | "stemmer" | "signal"> & {
70
+ accountIds?: string[];
71
+ };
31
72
  /** Notes matching a parsed query, by the same language as messages: files and written notes, merged. */
32
73
  export declare const searchNotes: (context: StoreContext, query: NoteQuery) => {
33
74
  items: NoteHit[];
34
75
  hasMore: boolean;
35
76
  };
77
+ export declare const searchCorpus: (context: StoreContext, query: CorpusQuery, corpus: Corpus) => CorpusRow[];
36
78
  export interface NoteChunkToEmbed {
37
79
  hash: string;
38
80
  text: string;
@@ -75,3 +117,4 @@ export interface NoteSearch extends DocumentPictureStore {
75
117
  }): Promise<NearestNote[]>;
76
118
  }
77
119
  export declare const noteSearchOver: (context: StoreContext) => NoteSearch;
120
+ export {};
@@ -40,9 +40,13 @@ const combine = (parts, operator) => ({
40
40
  const CORPORA = [
41
41
  {
42
42
  type: "document",
43
- view: "(SELECT id, account_id AS folder_id, external_id AS path, title, coalesce(body, '') AS text, metadata, updated_at, deleted_at FROM documents)",
43
+ noun: "note",
44
+ fields: NOTE_FIELDS,
45
+ view: "(SELECT id, account_id AS folder_id, external_id AS path, title, coalesce(body, '') AS text, metadata, updated_at AS at, deleted_at FROM documents)",
44
46
  words: "document_words",
45
47
  stems: "document_stems",
48
+ live: "coalesce(json_extract(n.metadata, '$.catalog.active'), 1)=1",
49
+ inAccounts: (ids) => bound("n.folder_id IN (SELECT CAST(value AS INTEGER) FROM json_each(?))", ids),
46
50
  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 = 'document') " +
47
51
  "OR n.folder_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 = 'account') " +
48
52
  "OR EXISTS (SELECT 1 FROM links l JOIN resolved_tags t ON t.id=l.to_id WHERE l.from_type = 'account' AND l.from_id = n.folder_id " +
@@ -50,7 +54,9 @@ const CORPORA = [
50
54
  },
51
55
  {
52
56
  type: "note",
53
- view: "(SELECT id, NULL AS folder_id, NULL AS path, title, body AS text, NULL AS metadata, updated_at, deleted_at FROM notes)",
57
+ noun: "note",
58
+ fields: NOTE_FIELDS,
59
+ view: "(SELECT id, NULL AS folder_id, NULL AS path, title, body AS text, NULL AS metadata, updated_at AS at, deleted_at FROM notes)",
54
60
  words: "note_words",
55
61
  stems: "note_stems",
56
62
  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),
@@ -61,14 +67,14 @@ export const searchNotes = (context, query) => {
61
67
  const offset = query.offset ?? 0;
62
68
  const chosen = CORPORA.filter(({ type }) => (query.source === undefined || (query.source === "file") === (type === "document")) &&
63
69
  (!query.folderIds?.length || query.withInternal === true || type === "document"));
64
- const rows = chosen.flatMap((corpus) => searchCorpus(context, query, corpus));
70
+ const rows = chosen.flatMap((corpus) => searchCorpus(context, { ...query, ...(query.folderIds ? { accountIds: query.folderIds } : {}) }, corpus));
65
71
  const exactFirst = rows.some((row) => row.exact !== undefined);
66
72
  rows.sort((a, b) => query.newest
67
- ? b.updatedAt - a.updatedAt || a.id - b.id
73
+ ? (b.at ?? 0) - (a.at ?? 0) || a.id - b.id
68
74
  : (exactFirst ? Number(b.exact === true) - Number(a.exact === true) : 0) ||
69
75
  Number(a.relevance === null) - Number(b.relevance === null) ||
70
76
  (a.relevance ?? 0) - (b.relevance ?? 0) ||
71
- b.updatedAt - a.updatedAt ||
77
+ (b.at ?? 0) - (a.at ?? 0) ||
72
78
  a.id - b.id);
73
79
  const read = {
74
80
  document: context.database.prepare("SELECT * FROM documents WHERE id = ?"),
@@ -76,7 +82,7 @@ export const searchNotes = (context, query) => {
76
82
  };
77
83
  return {
78
84
  items: rows.slice(offset, offset + query.limit).map((row) => {
79
- const found = read[row.type].get(row.id);
85
+ const found = read[row.type === "document" ? "document" : "note"].get(row.id);
80
86
  const note = row.type === "document" ? documentOf(found) : noteOf(found);
81
87
  return {
82
88
  ref: note.ref,
@@ -88,7 +94,7 @@ export const searchNotes = (context, query) => {
88
94
  hasMore: rows.length > offset + query.limit,
89
95
  };
90
96
  };
91
- const searchCorpus = (context, query, corpus) => {
97
+ export const searchCorpus = (context, query, corpus) => {
92
98
  const { database } = context;
93
99
  const wordMatch = (match) => ({
94
100
  sql: `n.id IN (SELECT rowid FROM ${corpus.words} WHERE ${corpus.words} MATCH ?)`,
@@ -116,8 +122,8 @@ const searchCorpus = (context, query, corpus) => {
116
122
  .map((row) => ({ id: Number(row.id), title: normalize(String(row.title)) })));
117
123
  const leaf = (node) => {
118
124
  const { field, operator, value } = node;
119
- if (!NOTE_FIELDS.includes(field))
120
- searchError("query.unsupported-field", node.span, `${field}: is not a note field — notes take ${NOTE_FIELDS.join(", ")}`);
125
+ if (!corpus.fields.includes(field))
126
+ searchError("query.unsupported-field", node.span, `${field}: is not ${corpus.noun === "event" ? "an" : "a"} ${corpus.noun} field — ${corpus.noun}s take ${corpus.fields.join(", ")}`);
121
127
  if ((field === "text" || field === "exact") && value === "")
122
128
  return bound("0");
123
129
  if (isStemmed(node)) {
@@ -168,7 +174,7 @@ const searchCorpus = (context, query, corpus) => {
168
174
  if (tag === undefined)
169
175
  searchError("query.invalid-tag", node.span);
170
176
  // A label on a folder or subfolder labels every note under it, at any depth.
171
- return corpus.tagged(canonicalTagNamed(database, tag)?.name ?? tag);
177
+ return corpus.tagged?.(canonicalTagNamed(database, tag)?.name ?? tag) ?? bound("0");
172
178
  }
173
179
  if (field === "source")
174
180
  return bound(value.toLowerCase() === (corpus.type === "note" ? "internal" : "file") ? "1" : "0");
@@ -199,15 +205,11 @@ const searchCorpus = (context, query, corpus) => {
199
205
  if (!range)
200
206
  searchError("query.invalid-ast", node.span);
201
207
  return combine([
202
- ...(range.lower === undefined
203
- ? []
204
- : [bound(`n.updated_at ${range.lower_inclusive ? ">=" : ">"} ?`, range.lower)]),
205
- ...(range.upper === undefined
206
- ? []
207
- : [bound(`n.updated_at ${range.upper_inclusive ? "<=" : "<"} ?`, range.upper)]),
208
+ ...(range.lower === undefined ? [] : [bound(`n.at ${range.lower_inclusive ? ">=" : ">"} ?`, range.lower)]),
209
+ ...(range.upper === undefined ? [] : [bound(`n.at ${range.upper_inclusive ? "<=" : "<"} ?`, range.upper)]),
208
210
  ], "AND");
209
211
  }
210
- return bound(["notes", "note"].includes(value.toLowerCase()) ? "1" : "0");
212
+ return bound([corpus.noun, `${corpus.noun}s`].includes(value.toLowerCase()) ? "1" : "0");
211
213
  };
212
214
  const compile = (node) => {
213
215
  if (node.kind === "predicate") {
@@ -243,10 +245,8 @@ const searchCorpus = (context, query, corpus) => {
243
245
  const stemmed = [...fragments.keys()].some(isStemmed);
244
246
  const scope = combine([
245
247
  bound("n.deleted_at IS NULL"),
246
- ...(corpus.type === "document" ? [bound("coalesce(json_extract(n.metadata, '$.catalog.active'), 1)=1")] : []),
247
- ...(query.folderIds?.length && corpus.type === "document"
248
- ? [bound("n.folder_id IN (SELECT CAST(value AS INTEGER) FROM json_each(?))", JSON.stringify(query.folderIds))]
249
- : []),
248
+ ...(corpus.live ? [bound(corpus.live)] : []),
249
+ ...(query.accountIds?.length && corpus.inAccounts ? [corpus.inAccounts(JSON.stringify(query.accountIds))] : []),
250
250
  ], "AND");
251
251
  const where = combine([scope, expression], "AND");
252
252
  const ranking = query.newest ? undefined : required(query.root, stemmed ? "stems" : "fts");
@@ -263,8 +263,8 @@ const searchCorpus = (context, query, corpus) => {
263
263
  cteParams.push(`normalized_text : (${exactTier})`);
264
264
  }
265
265
  const order = query.newest
266
- ? "n.updated_at DESC, n.id"
267
- : `${exactTier ? "exact DESC, " : ""}${ranking ? "f.rank IS NULL, f.rank, " : ""}n.updated_at DESC, n.id`;
266
+ ? "n.at DESC, n.id"
267
+ : `${exactTier ? "exact DESC, " : ""}${ranking ? "f.rank IS NULL, f.rank, " : ""}n.at DESC, n.id`;
268
268
  const offset = query.offset ?? 0;
269
269
  // With a JS test the SQL only names candidates: every leaf's SQL answer comes along, so the test sees it.
270
270
  const leaves = [...fragments.entries()].filter(([node]) => !tests.has(node));
@@ -304,7 +304,7 @@ const searchCorpus = (context, query, corpus) => {
304
304
  return matched.map((row) => ({
305
305
  type: corpus.type,
306
306
  id: Number(row.id),
307
- updatedAt: Number(row.updated_at),
307
+ at: row.at == null ? null : Number(row.at),
308
308
  relevance: row.relevance === null || row.relevance === undefined ? null : Number(row.relevance),
309
309
  ...(exactTier ? { exact: Number(row.exact) === 1 } : {}),
310
310
  }));
@@ -2,13 +2,13 @@ import type { Section } from "../../readers/types.js";
2
2
  import type { CacheDatabase } from "../driver.js";
3
3
  import { type DocumentMetadata } from "./document-metadata.js";
4
4
  import { type ImportedDocuments } from "./imported-documents.js";
5
- import { LINK_KINDS } from "./link-kinds.js";
5
+ import { LINK_KINDS, LINK_ORIGINS, type LINK_SOURCES } from "./link-kinds.js";
6
6
  import type { NoteSearch } from "./note-search.js";
7
7
  import type { StoreContext } from "./open.js";
8
8
  import { type Label } from "./tags.js";
9
9
  import { type Thing } from "./things.js";
10
10
  export declare const NOTE_FORMATS: readonly ["obsidian", "markdown"];
11
- export declare const LINK_ORIGINS: readonly ["file", "owner", "suggested", "message"];
11
+ export { LINK_ORIGINS } from "./link-kinds.js";
12
12
  /**
13
13
  * A notes folder: an `accounts` row of provider `local` or `obsidian`. Where it is on disk is this computer's config. A
14
14
  * documents account (Drive's, one per Google account) is read and written the same way and names its `account`.
@@ -52,7 +52,7 @@ export interface Link {
52
52
  to: string | null;
53
53
  kind: (typeof LINK_KINDS)[number];
54
54
  anchor: string | null;
55
- origin: (typeof LINK_ORIGINS)[number];
55
+ origin: (typeof LINK_SOURCES)[number];
56
56
  targetText: string | null;
57
57
  role: string | null;
58
58
  evidence: string | null;
@@ -76,7 +76,7 @@ export interface LinkInput {
76
76
  targetText?: string;
77
77
  kind: Link["kind"];
78
78
  anchor?: string;
79
- origin?: Link["origin"];
79
+ origin?: (typeof LINK_ORIGINS)[number];
80
80
  role?: string;
81
81
  evidence?: string;
82
82
  provenance?: string;
@@ -193,4 +193,3 @@ export declare const resolvePersonLinks: (database: CacheDatabase, names: (strin
193
193
  /** Writes one link; an identical resolved link is answered instead of doubled. */
194
194
  export declare const insertLink: (context: StoreContext, input: LinkInput) => Link;
195
195
  export declare const notesStoreOver: (context: StoreContext) => Omit<NotesStore, keyof NoteSearch>;
196
- export {};
@@ -10,13 +10,13 @@ import { ownerPerson } from "./actors.js";
10
10
  import { encodeDocumentMetadata, languageOf } from "./document-metadata.js";
11
11
  import { PREFERRED_FILE, syncFile } from "./files.js";
12
12
  import { importedDocumentWhere } from "./imported-documents.js";
13
- import { LINK_KINDS, linkKind } from "./link-kinds.js";
13
+ import { LINK_KINDS, LINK_ORIGINS, linkKind } from "./link-kinds.js";
14
14
  import { reviewLink } from "./link-reviews.js";
15
15
  import { addTags, ensureTag, labelsOf } from "./tags.js";
16
16
  import { canonicalThingReference, referenceOfThing, storedThing, thingOf } from "./things.js";
17
17
  import { write } from "./transaction.js";
18
18
  export const NOTE_FORMATS = ["obsidian", "markdown"];
19
- export const LINK_ORIGINS = ["file", "owner", "suggested", "message"];
19
+ export { LINK_ORIGINS } from "./link-kinds.js";
20
20
  const iso = (value) => new Date(Number(value)).toISOString();
21
21
  const optionalIso = (value) => (value == null ? null : iso(value));
22
22
  const textOf = (text, what, max = 100_000, allowEmpty = false) => {
@@ -23,7 +23,7 @@ export declare const projectAnnotationSnapshot: (database: CacheDatabase, projec
23
23
  ref: string;
24
24
  from: string;
25
25
  to: string | null;
26
- kind: "about" | "answered-by" | "assigned-to" | "blocked-by" | "created-from" | "duplicate-of" | "evidence" | "follow-up-of" | "labelled" | "links-to" | "member-of" | "related-to";
26
+ kind: "about" | "answered-by" | "assigned-to" | "attached" | "blocked-by" | "created-from" | "duplicate-of" | "evidence" | "follow-up-of" | "labelled" | "links-to" | "member-of" | "related-to";
27
27
  origin: "file" | "message" | "owner" | "suggested";
28
28
  anchor: string | null;
29
29
  target_text: string | null;
@@ -20,6 +20,7 @@ import { type AgentActionsStore } from "./sqlite/agent-actions.js";
20
20
  import { type BotUpdateStore } from "./sqlite/bot-updates.js";
21
21
  import { type CatalogStore } from "./sqlite/catalog.js";
22
22
  import { type MailStore } from "./sqlite/emails.js";
23
+ import { type EventSearch } from "./sqlite/event-search.js";
23
24
  import { type FactsStore } from "./sqlite/facts.js";
24
25
  import { type FilesStore } from "./sqlite/files.js";
25
26
  import { type InvolvementStore } from "./sqlite/involvements.js";
@@ -84,7 +85,7 @@ export interface MessageStore {
84
85
  /** Where each document is kept: its files, their folders and paths. */
85
86
  readonly files: FilesStore;
86
87
  /** Calendars and their entries, the source records of events: the `EventStore` a calendar CLI writes through. */
87
- readonly calendar: EventStore;
88
+ readonly calendar: EventStore & EventSearch;
88
89
  /** Choices that hold until replaced, each with its evidence. */
89
90
  readonly meetingFindings: MeetingFindingsStore;
90
91
  /** Cited facts and follow-ups about one project from stored messages, mail and documents, for review. */
@@ -19,6 +19,7 @@ import { eventStoreOver } from "./sqlite/calendar.js";
19
19
  import { catalogStoreOver } from "./sqlite/catalog.js";
20
20
  import { contextEvidenceOver, contextIndexOver, contextRecordsOver } from "./sqlite/context-evidence.js";
21
21
  import { mailStoreOver } from "./sqlite/emails.js";
22
+ import { eventSearchOver } from "./sqlite/event-search.js";
22
23
  import { evidenceFindingsStoreOver } from "./sqlite/evidence-findings.js";
23
24
  import { factsStoreOver } from "./sqlite/facts.js";
24
25
  import { filesStoreOver } from "./sqlite/files.js";
@@ -100,7 +101,7 @@ const storeOver = (context) => {
100
101
  const notes = once(() => ({ ...notesStoreOver(context), ...noteSearchOver(context) }));
101
102
  const catalog = once(() => catalogStoreOver(context));
102
103
  const files = once(() => filesStoreOver(context));
103
- const calendar = once(() => eventStoreOver(context));
104
+ const calendar = once(() => ({ ...eventStoreOver(context), ...eventSearchOver(context) }));
104
105
  const meetingFindings = once(() => meetingFindingsStoreOver(context));
105
106
  const evidenceFindings = once(() => evidenceFindingsStoreOver(context));
106
107
  const facts = once(() => factsStoreOver(context));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.452.0",
3
+ "version": "0.454.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",