@wirecat/cli-core 0.411.0 → 0.412.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 (53) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/cli/bot/command.js +3 -3
  3. package/dist/cli/bot/messages-sync.d.ts +7 -0
  4. package/dist/cli/bot/{store.js → messages-sync.js} +4 -5
  5. package/dist/cli/bot/port.d.ts +3 -3
  6. package/dist/cli/config-command.js +1 -0
  7. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  8. package/dist/cli/messenger/tasks-command.js +2 -2
  9. package/dist/cli/moved.js +1 -0
  10. package/dist/cli/settings.d.ts +2 -0
  11. package/dist/cli/settings.js +5 -1
  12. package/dist/domain/providers.d.ts +25 -3
  13. package/dist/domain/providers.js +89 -5
  14. package/dist/entities/index.d.ts +29 -1
  15. package/dist/entities/index.js +5 -1
  16. package/dist/entities/knowledge-lists.d.ts +28 -0
  17. package/dist/entities/knowledge-lists.js +29 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +1 -0
  20. package/dist/search/account-list.d.ts +5 -2
  21. package/dist/search/account-list.js +27 -11
  22. package/dist/search/query.js +3 -1
  23. package/dist/sends/permission-aliases.js +1 -0
  24. package/dist/services/documents/knowledge.d.ts +5 -3
  25. package/dist/services/documents/knowledge.js +4 -4
  26. package/dist/services/meeting-links.d.ts +3 -1
  27. package/dist/services/meeting-links.js +2 -2
  28. package/dist/services/messages-search.js +15 -3
  29. package/dist/services/search-all.js +5 -2
  30. package/dist/store/accounts.d.ts +3 -0
  31. package/dist/store/accounts.js +2 -0
  32. package/dist/store/exports.d.ts +1 -0
  33. package/dist/store/exports.js +1 -0
  34. package/dist/store/index.d.ts +3 -1
  35. package/dist/store/index.js +2 -0
  36. package/dist/store/knowledge-query.d.ts +6 -0
  37. package/dist/store/knowledge-query.js +34 -0
  38. package/dist/store/providers.d.ts +7 -0
  39. package/dist/store/providers.js +5 -0
  40. package/dist/store/sqlite/knowledge.d.ts +5 -4
  41. package/dist/store/sqlite/knowledge.js +50 -17
  42. package/dist/store/sqlite/mail-rules.d.ts +2 -1
  43. package/dist/store/sqlite/mail-rules.js +12 -2
  44. package/dist/store/sqlite/notes.d.ts +11 -5
  45. package/dist/store/sqlite/notes.js +9 -2
  46. package/dist/store/sqlite/open-store.js +2 -0
  47. package/dist/store/sqlite/providers.d.ts +12 -0
  48. package/dist/store/sqlite/providers.js +45 -0
  49. package/dist/store/sqlite/tags.d.ts +1 -1
  50. package/dist/store/sqlite/tags.js +3 -3
  51. package/package.json +1 -1
  52. package/parity.json +31 -35
  53. package/dist/cli/bot/store.d.ts +0 -7
@@ -1,5 +1,5 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
- import { FOLDER_PROVIDER, INTERNAL_NOTES, isKind, kindOf, PROVIDERS, providerNamed, } from "../domain/providers.js";
2
+ import { FOLDER_PROVIDER, INTERNAL_NOTES, isKind, kindIn, kindOf, providerNamed, providers as registered, UNKNOWN_PROVIDER, } from "../domain/providers.js";
3
3
  import { isBotProvider } from "./query.js";
4
4
  /** Store-wide: every profile, tg, MAX and memo reads the same groups. */
5
5
  export const ACCOUNT_GROUPS = "accountGroups";
@@ -25,16 +25,20 @@ export const groupMembers = (value) => itemsOf(value).map((item) => item.toLower
25
25
  ? fail(`"${item}" names no account after the colon`)
26
26
  : item);
27
27
  const describe = (accounts) => accounts.map(({ provider, account, name }) => `${provider}:${account}${name ? ` (${name})` : ""}`).join(", ");
28
- const heldOf = (stored) => [
28
+ /** The app running knows its own provider is a messenger, entry or not. */
29
+ const kindHere = (provider, running) => provider === running.provider ? kindOf(provider) : kindIn(provider);
30
+ const heldOf = (stored, running) => [
29
31
  ...stored
30
- .filter(({ provider }) => kindOf(provider) !== "notes")
31
- .map(({ provider, account, name }) => ({ provider, account, name, kind: kindOf(provider) })),
32
+ .filter(({ provider }) => kindIn(provider) !== "notes")
33
+ .map(({ provider, account, name }) => ({ provider, account, name, kind: kindHere(provider, running) })),
32
34
  { provider: "notes", account: INTERNAL_NOTES, name: null, kind: "notes" },
33
35
  ...stored
34
36
  .filter(({ provider }) => provider === FOLDER_PROVIDER)
35
37
  .map(({ id, name }) => ({ provider: "notes", account: String(id), name, kind: "notes" })),
36
38
  ];
37
- const registry = () => PROVIDERS.map(({ name, aliases }) => (aliases.length ? `${name} (${aliases.join(", ")})` : name)).join(", ");
39
+ const registry = () => registered()
40
+ .map(({ name, aliases }) => (aliases.length ? `${name} (${aliases.join(", ")})` : name))
41
+ .join(", ");
38
42
  const providerOf = (prefix, held, { running, profiles }) => {
39
43
  const wanted = prefix.toLowerCase();
40
44
  if (wanted === running.command?.toLowerCase())
@@ -57,7 +61,11 @@ const accountOf = (prefix, ref, held, context) => {
57
61
  const byId = ofProvider.find(({ account }) => account === ref || account === ref.toLowerCase());
58
62
  if (byId)
59
63
  return byId;
60
- const keyed = (key) => ofProvider.find(({ account }) => account === key.account) ?? { ...key, name: null, kind: kindOf(key.provider) };
64
+ const keyed = (key) => ofProvider.find(({ account }) => account === key.account) ?? {
65
+ ...key,
66
+ name: null,
67
+ kind: kindHere(key.provider, context.running),
68
+ };
61
69
  const recorded = context.profiles[prefix.toLowerCase()]?.[ref];
62
70
  if (recorded?.provider === FOLDER_PROVIDER) {
63
71
  const folder = held.find((one) => one.provider === "notes" && one.name === recorded.account);
@@ -105,7 +113,7 @@ const keyOf = ({ provider, account }) => `${provider}\u0000${account}`;
105
113
  * the account's name — a bare provider for all its accounts, or a bare kind for every provider of it.
106
114
  */
107
115
  export const entriesOfList = (value, context) => {
108
- const held = heldOf(context.held);
116
+ const held = heldOf(context.held, context.running);
109
117
  const entries = [];
110
118
  const entry = (item, group) => {
111
119
  const colon = item.indexOf(":");
@@ -161,6 +169,7 @@ const READS = {
161
169
  notes: "notes — search notes reads them",
162
170
  documents: "documents — search notes reads them",
163
171
  meetings: "meetings — search all reads them",
172
+ events: "calendar events — no search reads them yet",
164
173
  };
165
174
  /**
166
175
  * The accounts of `kinds` the entries name, and each entry that names none of them with the reason. A search
@@ -170,16 +179,23 @@ export const fitting = (entries, kinds, what, reasonOf = (kind) => READS[kind])
170
179
  const chosen = new Map();
171
180
  const skipped = [];
172
181
  for (const { entry, accounts } of entries) {
173
- const fits = accounts.filter(({ kind }) => kinds.includes(kind));
182
+ const fits = accounts.filter(({ kind }) => kind !== undefined && kinds.includes(kind));
174
183
  for (const { provider, account } of fits)
175
184
  if (!chosen.has(keyOf({ provider, account })))
176
185
  chosen.set(keyOf({ provider, account }), { provider, account });
186
+ const known = accounts.filter(({ kind }) => kind !== undefined);
187
+ const unknown = accounts.filter(({ kind }) => kind === undefined);
188
+ if (known.length)
189
+ for (const { provider, account } of unknown)
190
+ skipped.push({ source: `${provider}:${account}`, code: "not_found", reason: UNKNOWN_PROVIDER });
177
191
  if (fits.length)
178
192
  continue;
179
- const other = accounts[0];
180
- skipped.push(other
193
+ const other = known[0];
194
+ skipped.push(other?.kind
181
195
  ? { source: entry, code: "validation_error", reason: reasonOf(other.kind) }
182
- : { source: entry, code: "not_found", reason: "no account in the store" });
196
+ : unknown.length
197
+ ? { source: entry, code: "not_found", reason: UNKNOWN_PROVIDER }
198
+ : { source: entry, code: "not_found", reason: "no account in the store" });
183
199
  }
184
200
  if (chosen.size === 0)
185
201
  fail(`--source names nothing ${what} reads: ${skipped.map(({ source, reason }) => `${source} (${reason})`).join(", ")}`);
@@ -27,7 +27,9 @@ export const sourceOf = (name, value, providers) => {
27
27
  return source;
28
28
  if (isKind(source))
29
29
  fail(`${name}${source} names ${source}, which a message search does not read — ` +
30
- `search ${source === "meetings" ? "all --source meetings" : "notes"} does`);
30
+ (source === "events"
31
+ ? "no search reads them yet"
32
+ : `search ${source === "meetings" ? "all --source meetings" : "notes"} does`));
31
33
  const named = providerNamed(source);
32
34
  if (named && providers.includes(named.name))
33
35
  return named.name;
@@ -28,6 +28,7 @@ const RENAMED = [
28
28
  ["conversations.batches.next", "conversations.batches.show"],
29
29
  ["conversations.consents.revoke", "conversations.consents.remove"],
30
30
  ["stats.messages.counters.refresh", "stats.messages.counters.sync"],
31
+ ["store.fetch", "messages.sync"],
31
32
  ["store.gaps.plan", "store.gaps.show"],
32
33
  ["store.reindex", "store.rebuild"],
33
34
  ];
@@ -1,5 +1,5 @@
1
1
  import type { QueryAst } from "@wirecat/cli-base/query";
2
- import type { Author, Fact, Link, LinkInput, Memory, MemoryInput, MessageStore, Note } from "../../store/index.js";
2
+ import type { Author, Fact, Link, LinkInput, LinkListFilter, Memory, MemoryInput, MessageStore, Note } from "../../store/index.js";
3
3
  export interface DocumentKnowledge {
4
4
  documents(principal: string, query: QueryAst | undefined, page: {
5
5
  limit: number;
@@ -9,8 +9,10 @@ export interface DocumentKnowledge {
9
9
  hasMore: boolean;
10
10
  }>;
11
11
  document(principal: string, ref: string): Promise<Note>;
12
- tags(principal: string, ref: string, action: "list" | "add" | "remove", tags?: string[]): Promise<string[]>;
13
- links(principal: string, ref: string): Promise<Link[]>;
12
+ tags(principal: string, ref: string, action: "list" | "add" | "remove", tags?: string[], filter?: {
13
+ tag?: string;
14
+ }): Promise<string[]>;
15
+ links(principal: string, ref: string, filter?: Omit<LinkListFilter, "from" | "to">): Promise<Link[]>;
14
16
  addLink(principal: string, ref: string, input: Omit<LinkInput, "from" | "origin">): Promise<Link>;
15
17
  removeLink(principal: string, ref: string, id: string): Promise<unknown>;
16
18
  notes(principal: string, ref: string, options?: {
@@ -47,18 +47,18 @@ export const documentKnowledge = (store, folder) => {
47
47
  return folderId ? store.notes.importedDocuments(folderId, query, page) : { items: [], hasMore: false };
48
48
  },
49
49
  document,
50
- tags: async (principal, ref, action, tags = []) => {
50
+ tags: async (principal, ref, action, tags = [], filter) => {
51
51
  const note = await document(principal, ref);
52
52
  const target = { type: "document", id: note.id };
53
53
  if (action === "add")
54
54
  return store.knowledge.addTags(null, target, tags);
55
55
  if (action === "remove")
56
56
  return store.knowledge.removeTags(null, target, tags);
57
- return store.knowledge.tags(null, target);
57
+ return store.knowledge.tags(null, target, filter?.tag);
58
58
  },
59
- links: async (principal, ref) => {
59
+ links: async (principal, ref, filter) => {
60
60
  await document(principal, ref);
61
- return store.notes.links({ from: ref });
61
+ return store.notes.links({ ...filter, from: ref });
62
62
  },
63
63
  addLink: async (principal, ref, input) => {
64
64
  await document(principal, ref);
@@ -1,7 +1,9 @@
1
1
  import type { MeetingChatLinesInput, MeetingHitSearchRequest, MeetingLink } from "../meetings/index.js";
2
2
  import type { MessageStore } from "../store/store.js";
3
3
  export type { MeetingLink };
4
- export type MeetingLinksRequest = Omit<MeetingHitSearchRequest, "limit" | "after" | "maxReadBytes"> & Pick<MeetingChatLinesInput, "maxMessages">;
4
+ export type MeetingLinksRequest = Omit<MeetingHitSearchRequest, "limit" | "after" | "maxReadBytes"> & Pick<MeetingChatLinesInput, "maxMessages"> & {
5
+ url?: string;
6
+ };
5
7
  /** Links shared in the chat of the selected meetings, most shared first; found when read, not stored. */
6
8
  export declare const readMeetingLinks: (store: Pick<MessageStore, "meetings"> & {
7
9
  knowledge?: MessageStore["knowledge"];
@@ -3,7 +3,7 @@ import { findMentions } from "../domain/mentions.js";
3
3
  import { resolveMeetingSelection } from "./meeting-hit-search.js";
4
4
  /** Links shared in the chat of the selected meetings, most shared first; found when read, not stored. */
5
5
  export const readMeetingLinks = async (store, request) => {
6
- const { maxMessages, ...selecting } = request;
6
+ const { maxMessages, url, ...selecting } = request;
7
7
  const { input, selection } = await resolveMeetingSelection(store, selecting);
8
8
  const lines = await store.meetings.meetingChatLines({
9
9
  ...input,
@@ -12,7 +12,7 @@ export const readMeetingLinks = async (store, request) => {
12
12
  const links = new Map();
13
13
  for (const line of lines)
14
14
  for (const found of findMentions(line.text)) {
15
- if (found.kind !== "url")
15
+ if (found.kind !== "url" || (url !== undefined && found.value !== url))
16
16
  continue;
17
17
  const link = links.get(found.value) ?? {
18
18
  url: found.value,
@@ -1,6 +1,7 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { calendarKey } from "../domain/calendar.js";
3
3
  import { parseLocator } from "../domain/locator.js";
4
+ import { kindIn, UNKNOWN_PROVIDER } from "../domain/providers.js";
4
5
  import { syncsOneChat } from "../ports/traits.js";
5
6
  import { PRESET_VERSION } from "../search/lucene/presets.js";
6
7
  import { FIELD_VERSION, validateAst, validateFields } from "../search/lucene/registry.js";
@@ -69,13 +70,24 @@ export const prepareLucene = async (store, account, request, messenger) => {
69
70
  throw new CliError("validation_error", "a search names at least one account");
70
71
  if (request.senders !== undefined && leaves.some(({ field }) => field === "from"))
71
72
  throw new CliError("validation_error", "--from and from: together — name the people once");
72
- const accounts = accountsOfKind(request.kind, request.accounts ??
73
+ const chosen = request.accounts ??
73
74
  listed?.accounts ??
74
75
  (source !== undefined
75
76
  ? held.filter(({ provider }) => inSource(source, provider))
76
77
  : sources.length
77
78
  ? held.filter(({ provider }) => sources.some((source) => inSource(source, provider)))
78
- : [account]), held, source ?? (sources.includes("email") ? "email" : sources[0]), messenger.app?.command, request.mailAccount, listed !== undefined || request.accounts !== undefined);
79
+ : [account]);
80
+ // The account running is the app's own: its provider is a messenger whether or not an entry names it.
81
+ const unknown = chosen.filter(({ provider }) => provider !== account.provider && kindIn(provider) === undefined);
82
+ const unknownSkipped = unknown.map(({ provider, account }) => ({
83
+ source: `${provider}:${account}`,
84
+ code: "not_found",
85
+ reason: UNKNOWN_PROVIDER,
86
+ }));
87
+ if (unknown.length > 0 && unknown.length === chosen.length)
88
+ throw new CliError("not_found", `this search reads nothing: ${unknownSkipped.map(({ source }) => source).join(", ")} (${UNKNOWN_PROVIDER})`);
89
+ const skipped = [...(listed?.skipped ?? []), ...unknownSkipped];
90
+ const accounts = accountsOfKind(request.kind, chosen.filter((one) => !unknown.includes(one)), held, source ?? (sources.includes("email") ? "email" : sources[0]), messenger.app?.command, request.mailAccount, listed !== undefined || request.accounts !== undefined);
79
91
  const scopeAccounts = accounts.map(({ provider, account }) => ({ provider, account }));
80
92
  const chatLookup = scopeAccounts.some(({ provider }) => provider === MAIL) ? withMailThreads(store) : store;
81
93
  const mailField = leaves.find(({ field, value }) => MAIL_FIELDS.has(field) &&
@@ -141,7 +153,7 @@ export const prepareLucene = async (store, account, request, messenger) => {
141
153
  stemsReady,
142
154
  ...(stemmer ? { stemming: stemmingOf(ast.root, stemmer) } : unstemmed ? { stemming: unstemmed } : {}),
143
155
  scopeAccounts,
144
- ...(listed?.skipped.length ? { skipped: listed.skipped } : {}),
156
+ ...(skipped.length ? { skipped } : {}),
145
157
  ...(selectedChat ? { selectedChat } : {}),
146
158
  };
147
159
  };
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { parseLocator } from "../domain/locator.js";
3
- import { INTERNAL_NOTES, kindOf } from "../domain/providers.js";
3
+ import { INTERNAL_NOTES, kindIn } from "../domain/providers.js";
4
4
  import { excerpt } from "./excerpt.js";
5
5
  import { MAX_SEARCH_OFFSET, searchStore } from "./messages-archive-search.js";
6
6
  import { notesScopeOf, RRF_K, searchNotes } from "./notes-search.js";
@@ -55,7 +55,10 @@ export const searchAll = async (store, account, request, messenger = {}, searchM
55
55
  hasMore ||= found.hasMore;
56
56
  const read = found.coverage?.accounts ??
57
57
  given ??
58
- (await store.accounts.accounts()).filter(({ provider }) => kindOf(provider) === kind);
58
+ (await store.accounts.accounts()).filter(({ provider }) => kindIn(provider) === kind);
59
+ for (const one of found.skipped ?? [])
60
+ if (!skipped.some(({ source }) => source === one.source))
61
+ skipped.push(one);
59
62
  accounts.push(...read.map(({ provider, account }) => ({ resource: kind, provider, account })));
60
63
  return found.items.map((hit) => {
61
64
  const at = parseLocator(hit.locator);
@@ -1,3 +1,4 @@
1
+ import type { ProviderEntry } from "../domain/providers.js";
1
2
  import { type AccountGroups, type Profiles } from "../search/account-list.js";
2
3
  import type { StoreContext } from "./sqlite/open.js";
3
4
  import type { AccountKey, StoredAccount } from "./types.js";
@@ -29,6 +30,8 @@ export interface AccountsStore extends AccountAccounts {
29
30
  profiles(): Promise<Profiles>;
30
31
  /** Records which account a profile of `app` is; a no-op when the store already says so. */
31
32
  saveProfile(app: string, profile: string, key: AccountKey): Promise<void>;
33
+ /** Writes a CLI's provider entry for every tool on this store to read; see `registerProvider`. */
34
+ registerProvider(entry: ProviderEntry): Promise<void>;
32
35
  }
33
36
  export declare const accountAccountsOver: (context: StoreContext) => AccountAccounts;
34
37
  export declare const accountsStoreOver: (context: StoreContext, keyed: AccountAccounts) => AccountsStore;
@@ -3,6 +3,7 @@ import { keysOver } from "./keys.js";
3
3
  import { accountGroupsOf, profilesOf, saveProfile } from "./sqlite/account-groups.js";
4
4
  import * as accounts from "./sqlite/accounts.js";
5
5
  import { linkOwnIdentities } from "./sqlite/own-identities.js";
6
+ import { registerProvider } from "./sqlite/providers.js";
6
7
  import { linkInbox } from "./sqlite/tasks.js";
7
8
  import { write } from "./sqlite/transaction.js";
8
9
  export const accountAccountsOver = (context) => {
@@ -34,6 +35,7 @@ export const accountsStoreOver = (context, keyed) => {
34
35
  accountGroups: async () => accountGroupsOf(database),
35
36
  profiles: async () => profilesOf(database),
36
37
  saveProfile: async (app, profile, key) => write(database, () => saveProfile(database, app, profile, key, context.now())),
38
+ registerProvider: async (entry) => registerProvider(database, entry, context.now()),
37
39
  saveAccountGroup: async (name, members) => write(database, () => {
38
40
  const groups = accountGroupsOf(database);
39
41
  if (members === undefined)
@@ -8,6 +8,7 @@ export type { AccountCounters, CountersStore } from "./counters.js";
8
8
  export type { AccountMentions, MentionsStore } from "./mentions.js";
9
9
  export type { AccountMessages, MessagesStore } from "./messages.js";
10
10
  export type { AccountPeople, PeopleStore } from "./people.js";
11
+ export { registerProvider } from "./providers.js";
11
12
  export type { QueriesStore } from "./queries.js";
12
13
  export type { RosterStore } from "./roster.js";
13
14
  export type { SearchIndexStore } from "./search-index.js";
@@ -1,3 +1,4 @@
1
+ export { registerProvider } from "./providers.js";
1
2
  export { accountGroupsOf } from "./sqlite/account-groups.js";
2
3
  export { attachmentColumns, resetAttachmentWords, } from "./sqlite/attachment-texts.js";
3
4
  export { pendingNormalization } from "./sqlite/backfill.js";
@@ -3,6 +3,7 @@ export { FACT_STATUSES, FACT_TYPES, MEMORY_KINDS, MEMORY_SCOPES, MEMORY_STATUSES
3
3
  export type { TagConsolidationOptions, TagConsolidationPreview, TagConsolidationStore, } from "../domain/tag-consolidation.js";
4
4
  export type { TagRedirectIndex, TagRedirectResolution, TagRedirectSnapshot } from "../domain/tag-redirects.js";
5
5
  export { createTagRedirectIndex } from "../domain/tag-redirects.js";
6
+ export { note } from "../entities/note.js";
6
7
  export type { FindingEvidence, FindingOutcome, MeetingFactFinding, MeetingFinding, MeetingFindingsInput, MeetingFindingsReceipt, MeetingTaskFinding, } from "../meetings/index.js";
7
8
  export type { AccountMail, AccountMeetings, AccountStore } from "./account-store.js";
8
9
  export type { DocumentTransferOptions, DocumentTransferPreview, DocumentTransferReceipt, } from "./document-transfer/index.js";
@@ -10,6 +11,7 @@ export { applyDocumentTransfer, previewDocumentTransfer } from "./document-trans
10
11
  export type { CacheDatabase, CacheStatement, OpenDatabase, SqlValue } from "./driver.js";
11
12
  export { PRAGMAS } from "./driver.js";
12
13
  export { EVIDENCE_FINDINGS_KIND, type EvidenceFindingsBatch, type EvidenceFindingsDecider, type EvidenceFindingsReceipt, type EvidenceFindingsStore, validateEvidenceFindings, } from "./evidence-findings.js";
14
+ export { ORGANIZATIONS_QUERY, organization, PROJECTS_QUERY, project, REMINDERS_QUERY, reminder, } from "./knowledge-query.js";
13
15
  export type { MeetingFindingsBatch, MeetingFindingsStore } from "./meeting-findings.js";
14
16
  export { MIGRATIONS, type Migration, migrate } from "./migrations.js";
15
17
  export { openCache } from "./open.js";
@@ -41,7 +43,7 @@ export type { AttachmentInput } from "./sqlite/meetings.js";
41
43
  export type { Author, MemoriesStore, Memory, MemoryInput } from "./sqlite/memories.js";
42
44
  export type { NoteIndexState } from "./sqlite/note-index.js";
43
45
  export { type NearestNote, NOTE_FIELDS, type NoteChunkToEmbed, type NoteHit, type NoteQuery, type NoteSearch, } from "./sqlite/note-search.js";
44
- export type { FileNoteInput, Link, LinkInput, Note, NoteFolder, NotesStore, NoteTag, } from "./sqlite/notes.js";
46
+ export type { FileNoteInput, Link, LinkInput, LinkListFilter, Note, NoteFolder, NotesStore, NoteTag, } from "./sqlite/notes.js";
45
47
  export type { PrivateContact, PrivateContactNote } from "./sqlite/private-people.js";
46
48
  export type { ProposalInput, ProposedAction, ProposedActionsStore } from "./sqlite/proposed-actions.js";
47
49
  export { PROPOSAL_STATUSES } from "./sqlite/proposed-actions.js";
@@ -1,8 +1,10 @@
1
1
  export { FACT_STATUSES, FACT_TYPES, MEMORY_KINDS, MEMORY_SCOPES, MEMORY_STATUSES, TASK_TYPES, WORK_TASK_TYPES, } from "../domain/knowledge.js";
2
2
  export { createTagRedirectIndex } from "../domain/tag-redirects.js";
3
+ export { note } from "../entities/note.js";
3
4
  export { applyDocumentTransfer, previewDocumentTransfer } from "./document-transfer/index.js";
4
5
  export { PRAGMAS } from "./driver.js";
5
6
  export { EVIDENCE_FINDINGS_KIND, validateEvidenceFindings, } from "./evidence-findings.js";
7
+ export { ORGANIZATIONS_QUERY, organization, PROJECTS_QUERY, project, REMINDERS_QUERY, reminder, } from "./knowledge-query.js";
6
8
  export { MIGRATIONS, migrate } from "./migrations.js";
7
9
  export { openCache } from "./open.js";
8
10
  export { storePath } from "./path.js";
@@ -0,0 +1,6 @@
1
+ import { type QueryRegistry } from "@wirecat/cli-base/query";
2
+ import { organization, project, reminder } from "../entities/knowledge-lists.js";
3
+ export { organization, project, reminder };
4
+ export declare const ORGANIZATIONS_QUERY: QueryRegistry;
5
+ export declare const PROJECTS_QUERY: QueryRegistry;
6
+ export declare const REMINDERS_QUERY: QueryRegistry;
@@ -0,0 +1,34 @@
1
+ import { queryFieldsOf } from "@wirecat/cli-base/query";
2
+ import { organization, project, reminder } from "../entities/knowledge-lists.js";
3
+ export { organization, project, reminder };
4
+ export const ORGANIZATIONS_QUERY = {
5
+ entity: "organization",
6
+ backend: "sql",
7
+ fields: queryFieldsOf(organization, {
8
+ native: ["id", "kind", "scope", "created_at"],
9
+ examples: { id: "id:1", kind: "kind:company", scope: "scope:work", created_at: "created_at>=2026-01-01" },
10
+ }),
11
+ };
12
+ export const PROJECTS_QUERY = {
13
+ entity: "project",
14
+ backend: "sql",
15
+ fields: queryFieldsOf(project, {
16
+ native: ["id", "key", "type", "organization_id", "scope", "created_at"],
17
+ examples: {
18
+ id: "id:1",
19
+ key: "key:SMP",
20
+ type: "type:work",
21
+ organization_id: "organization_id:1",
22
+ scope: "scope:work",
23
+ created_at: "created_at>=2026-01-01",
24
+ },
25
+ }),
26
+ };
27
+ export const REMINDERS_QUERY = {
28
+ entity: "reminder",
29
+ backend: "sql",
30
+ fields: queryFieldsOf(reminder, {
31
+ native: ["id", "state", "due_at", "revision"],
32
+ examples: { id: "id:1", state: "state:pending", due_at: "due_at>=2026-01-01", revision: "revision>1" },
33
+ }),
34
+ };
@@ -0,0 +1,7 @@
1
+ import type { ProviderEntry } from "../domain/providers.js";
2
+ import type { MessageStore } from "./store.js";
3
+ /**
4
+ * A CLI's entry, written into the store on every run so cli-core in every other CLI knows its provider's kind and
5
+ * aliases. Two entries that disagree on a kind, or an alias another provider already has, are an error.
6
+ */
7
+ export declare const registerProvider: (store: Pick<MessageStore, "accounts">, entry: ProviderEntry) => Promise<void>;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * A CLI's entry, written into the store on every run so cli-core in every other CLI knows its provider's kind and
3
+ * aliases. Two entries that disagree on a kind, or an alias another provider already has, are an error.
4
+ */
5
+ export const registerProvider = (store, entry) => store.accounts.registerProvider(entry);
@@ -1,3 +1,4 @@
1
+ import type { QueryAst } from "@wirecat/cli-base/query";
1
2
  import type { AccountKey } from "../types.js";
2
3
  import { RELATION_KINDS } from "./link-kinds.js";
3
4
  import { type MeetingProjectsStore } from "./meeting-projects.js";
@@ -130,7 +131,7 @@ export interface KnowledgeStore extends VocabularyStore, MeetingProjectsStore {
130
131
  id: string;
131
132
  removed: true;
132
133
  }>;
133
- tags(key: KnowledgeScope, target: KnowledgeTarget): Promise<string[]>;
134
+ tags(key: KnowledgeScope, target: KnowledgeTarget, tag?: string): Promise<string[]>;
134
135
  addTags(key: KnowledgeScope, target: KnowledgeTarget, tags: string[]): Promise<string[]>;
135
136
  removeTags(key: KnowledgeScope, target: KnowledgeTarget, tags: string[]): Promise<string[]>;
136
137
  /**
@@ -163,7 +164,7 @@ export interface KnowledgeStore extends VocabularyStore, MeetingProjectsStore {
163
164
  name: string;
164
165
  scope?: Organization["scope"];
165
166
  }): Promise<Organization>;
166
- organizations(): Promise<Organization[]>;
167
+ organizations(query?: QueryAst): Promise<Organization[]>;
167
168
  /** A project; its key is made from the name when none is given. */
168
169
  addProject(input: {
169
170
  name: string;
@@ -173,7 +174,7 @@ export interface KnowledgeStore extends VocabularyStore, MeetingProjectsStore {
173
174
  scope?: Project["scope"];
174
175
  description?: string;
175
176
  }): Promise<Project>;
176
- projects(): Promise<Project[]>;
177
+ projects(query?: QueryAst): Promise<Project[]>;
177
178
  relate(key: KnowledgeScope, input: {
178
179
  from: string;
179
180
  to: string;
@@ -190,7 +191,7 @@ export interface KnowledgeStore extends VocabularyStore, MeetingProjectsStore {
190
191
  }>;
191
192
  confirmRelation(key: KnowledgeScope, id: string): Promise<KnowledgeRelation>;
192
193
  schedule(key: AccountKey, task: string, dueAt: string, timezone: string): Promise<Reminder>;
193
- reminders(key: AccountKey): Promise<Reminder[]>;
194
+ reminders(key: AccountKey, query?: QueryAst): Promise<Reminder[]>;
194
195
  cancelReminder(key: AccountKey, id: string): Promise<Reminder>;
195
196
  snoozeReminder(key: AccountKey, id: string, dueAt: string, revision: number): Promise<Reminder>;
196
197
  claimReminders(key: AccountKey, options?: {
@@ -9,6 +9,7 @@ import { ownerPerson } from "./actors.js";
9
9
  import { linkKind, RELATION_KINDS } from "./link-kinds.js";
10
10
  import { meetingProjectsStoreOver } from "./meeting-projects.js";
11
11
  import { insertLink, linkOf } from "./notes.js";
12
+ import { queryFilter } from "./query-filter.js";
12
13
  import { addTags as addThingTags, canonicalTagNamed, createTag, labelsOf, removeTags as removeThingTags, setMainTopic, } from "./tags.js";
13
14
  import { TASK_KEY_PREFIXES } from "./task-keys.js";
14
15
  import { inboxKey, taskAccountOf, taskIdOf, taskRowOf } from "./tasks.js";
@@ -153,10 +154,10 @@ export const knowledgeStoreOver = (context) => {
153
154
  ? parseReference(formatReference({ type: "folder", id: target.id, path: target.path ?? null })).path
154
155
  : null;
155
156
  /** A subfolder's labels: `labelled` links from the folder, anchored at the path, to each tag. */
156
- const subfolderLabels = (folder, path) => database
157
+ const subfolderLabels = (folder, path, tag) => database
157
158
  .prepare("SELECT t.name FROM links l JOIN tags t ON t.id = l.to_id WHERE l.from_type = 'account' AND l.from_id = ? " +
158
- `AND l.kind = ${linkKind("labelled")} AND l.to_type = 'tag' AND l.anchor = ? ORDER BY t.name`)
159
- .all(folder, path)
159
+ `AND l.kind = ${linkKind("labelled")} AND l.to_type = 'tag' AND l.anchor = ?${tag === undefined ? "" : " AND t.name = ?"} ORDER BY t.name`)
160
+ .all(folder, path, ...(tag === undefined ? [] : [tag]))
160
161
  .map((row) => String(row.name));
161
162
  const annotationOf = (row) => {
162
163
  const thing = storedThing(row.notable_type, row.notable_id);
@@ -335,12 +336,15 @@ export const knowledgeStoreOver = (context) => {
335
336
  database.prepare("DELETE FROM notes WHERE id=?").run(Number(id));
336
337
  return { id, removed: true };
337
338
  },
338
- tags: async (key, target) => {
339
+ tags: async (key, target, tag) => {
339
340
  const { thing, state } = resolve(key, target);
340
341
  if (!thing || state !== "available")
341
342
  return [];
342
343
  const path = subfolder(target);
343
- return path === null ? labelsOf(database, thing).map(({ tag }) => tag) : subfolderLabels(thing.id, path);
344
+ const name = tag === undefined ? undefined : canonicalTagName(tag);
345
+ return path === null
346
+ ? labelsOf(database, thing, name).map(({ tag }) => tag)
347
+ : subfolderLabels(thing.id, path, name);
344
348
  },
345
349
  addTags: async (key, target, tags) => {
346
350
  const thing = available(key, target);
@@ -418,10 +422,19 @@ export const knowledgeStoreOver = (context) => {
418
422
  .get(oneOf(kind, ORGANIZATION_KINDS, "an organization's kind"), textOf(name, 200), oneOf(scope, SCOPES, "a scope"), at, at);
419
423
  return organizationOf(row);
420
424
  },
421
- organizations: async () => database
422
- .prepare("SELECT * FROM organizations WHERE deleted_at IS NULL ORDER BY name, id LIMIT 500")
423
- .all()
424
- .map(organizationOf),
425
+ organizations: async (query) => {
426
+ const where = query &&
427
+ queryFilter(query, {
428
+ id: { type: "kw", sql: "CAST(id AS TEXT)" },
429
+ kind: { type: "enum", sql: "kind" },
430
+ scope: { type: "enum", sql: "scope" },
431
+ created_at: { type: "time", sql: "created_at" },
432
+ });
433
+ return database
434
+ .prepare(`SELECT * FROM organizations WHERE deleted_at IS NULL${where ? ` AND (${where.sql})` : ""} ORDER BY name, id LIMIT 500`)
435
+ .all(...(where?.params ?? []))
436
+ .map(organizationOf);
437
+ },
425
438
  addProject: async ({ name, key, type = "other", organization, scope = "personal", description }) => write(database, () => {
426
439
  const title = textOf(name, 200);
427
440
  const organizationId = organization === undefined
@@ -475,10 +488,21 @@ export const knowledgeStoreOver = (context) => {
475
488
  .get(chosen, title, description === undefined ? null : textOf(description, 2000), oneOf(type, PROJECT_TYPES, "a project's type"), organizationId, oneOf(scope, SCOPES, "a scope"), at, at);
476
489
  return projectOf(row);
477
490
  }),
478
- projects: async () => database
479
- .prepare("SELECT * FROM projects WHERE deleted_at IS NULL ORDER BY name, id LIMIT 500")
480
- .all()
481
- .map(projectOf),
491
+ projects: async (query) => {
492
+ const where = query &&
493
+ queryFilter(query, {
494
+ id: { type: "kw", sql: "CAST(id AS TEXT)" },
495
+ key: { type: "kw", sql: "key" },
496
+ type: { type: "enum", sql: "type" },
497
+ organization_id: { type: "kw", sql: "CAST(organization_id AS TEXT)" },
498
+ scope: { type: "enum", sql: "scope" },
499
+ created_at: { type: "time", sql: "created_at" },
500
+ });
501
+ return database
502
+ .prepare(`SELECT * FROM projects WHERE deleted_at IS NULL${where ? ` AND (${where.sql})` : ""} ORDER BY name, id LIMIT 500`)
503
+ .all(...(where?.params ?? []))
504
+ .map(projectOf);
505
+ },
482
506
  relate: async (key, input) => {
483
507
  const from = related(key, input.from, true);
484
508
  const to = related(key, input.to);
@@ -553,10 +577,19 @@ export const knowledgeStoreOver = (context) => {
553
577
  .get(row, accountId, timestamp, timezone, at, at);
554
578
  return reminder(key, String(made?.id));
555
579
  },
556
- reminders: async (key) => database
557
- .prepare("SELECT * FROM reminders WHERE account_id=? ORDER BY due_at, id LIMIT 500")
558
- .all(account(key))
559
- .map(reminderOf),
580
+ reminders: async (key, query) => {
581
+ const where = query &&
582
+ queryFilter(query, {
583
+ id: { type: "kw", sql: "CAST(id AS TEXT)" },
584
+ state: { type: "enum", sql: "state" },
585
+ due_at: { type: "time", sql: "due_at" },
586
+ revision: { type: "num", sql: "revision" },
587
+ });
588
+ return database
589
+ .prepare(`SELECT * FROM reminders WHERE account_id=?${where ? ` AND (${where.sql})` : ""} ORDER BY due_at, id LIMIT 500`)
590
+ .all(account(key), ...(where?.params ?? []))
591
+ .map(reminderOf);
592
+ },
560
593
  cancelReminder: async (key, id) => {
561
594
  reminder(key, id);
562
595
  database
@@ -1,3 +1,4 @@
1
+ import type { QueryAst } from "@wirecat/cli-base/query";
1
2
  import type { JsonValue } from "../../meetings/index.js";
2
3
  import type { CacheDatabase } from "../driver.js";
3
4
  type JsonObject = {
@@ -36,7 +37,7 @@ export interface MailRuleCount {
36
37
  }
37
38
  export interface MailRulesStore {
38
39
  /** Oldest first. */
39
- list(accountId: number): Promise<MailRule[]>;
40
+ list(accountId: number, query?: QueryAst): Promise<MailRule[]>;
40
41
  /** A rule with the same conditions is replaced, and keeps its id and the emails it decided. */
41
42
  upsert(accountId: number, rule: MailRuleInput, now: number): Promise<MailRule>;
42
43
  /** Clears the rule from the emails it decided. Answers false when no rule has these conditions. */
@@ -1,3 +1,4 @@
1
+ import { queryFilter } from "./query-filter.js";
1
2
  import { write } from "./transaction.js";
2
3
  const sorted = (value) => Array.isArray(value)
3
4
  ? value.map(sorted)
@@ -23,8 +24,17 @@ const ruleOf = (row) => ({
23
24
  // SQLite allows 32766 parameters; two are taken by the account and the rule.
24
25
  const CHUNK = 30_000;
25
26
  export const mailRulesOver = (database) => ({
26
- async list(accountId) {
27
- return database.prepare("SELECT * FROM mail_rules WHERE account_id = ? ORDER BY id").all(accountId).map(ruleOf);
27
+ async list(accountId, query) {
28
+ const where = query &&
29
+ queryFilter(query, {
30
+ id: { type: "num", sql: "id" },
31
+ protected: { type: "bool", sql: "protected" },
32
+ source: { type: "kw", sql: "source" },
33
+ });
34
+ return database
35
+ .prepare(`SELECT * FROM mail_rules WHERE account_id = ?${where ? ` AND (${where.sql})` : ""} ORDER BY id`)
36
+ .all(accountId, ...(where?.params ?? []))
37
+ .map(ruleOf);
28
38
  },
29
39
  async upsert(accountId, rule, now) {
30
40
  const row = database
@@ -51,6 +51,16 @@ export interface Link {
51
51
  confirmed: boolean;
52
52
  createdAt: string;
53
53
  }
54
+ export interface LinkListFilter {
55
+ from?: string;
56
+ to?: string;
57
+ unresolved?: boolean;
58
+ id?: string;
59
+ kind?: Link["kind"];
60
+ role?: string;
61
+ origin?: Link["origin"];
62
+ confirmed?: boolean;
63
+ }
54
64
  export interface LinkInput {
55
65
  from: string;
56
66
  to?: string;
@@ -123,11 +133,7 @@ export interface NotesStore extends NoteSearch, ImportedDocuments {
123
133
  removed: true;
124
134
  }>;
125
135
  addLink(input: LinkInput): Promise<Link>;
126
- links(options?: {
127
- from?: string;
128
- to?: string;
129
- unresolved?: boolean;
130
- }): Promise<Link[]>;
136
+ links(options?: LinkListFilter): Promise<Link[]>;
131
137
  removeLink(id: string): Promise<{
132
138
  id: string;
133
139
  removed: true;