@wirecat/cli-core 0.588.0 → 0.589.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/cli/messenger/search-answers.js +3 -0
- package/dist/domain/providers.d.ts +7 -1
- package/dist/domain/providers.js +3 -1
- package/dist/entities/hit.d.ts +2 -2
- package/dist/entities/hit.js +2 -2
- package/dist/entities/index.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/search/account-list.js +1 -0
- package/dist/services/project-status/index.d.ts +1 -1
- package/dist/services/project-status/index.js +13 -12
- package/dist/services/project-status/signals/amounts.d.ts +1 -0
- package/dist/services/project-status/signals/amounts.js +4 -3
- package/dist/services/project-status/types.d.ts +2 -0
- package/dist/services/search-all-meetings.d.ts +1 -1
- package/dist/services/search-all-meetings.js +1 -0
- package/dist/services/search-all.d.ts +15 -5
- package/dist/services/search-all.js +46 -3
- package/dist/store/sqlite/deals.d.ts +14 -0
- package/dist/store/sqlite/deals.js +25 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.589.0 — 06.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`crm` is a provider kind, and `search all` reads deals and leads.** A CRM CLI registers its provider with
|
|
15
|
+
`kind: "crm"` and `opens: CRM_OPENS` (`deals show`, `leads show`, `pipelines show`). `searchAll` has a resource
|
|
16
|
+
`crm` that finds deals and leads, the owner's own and every CRM account's copies, by the words of their title in
|
|
17
|
+
any letter case; its hits are `deal` and `lead`, their title and excerpt marked untrusted. A query with a field,
|
|
18
|
+
a wildcard or NOT skips `crm` with its reason. `--source` and a profile lock narrow it to the accounts named.
|
|
19
|
+
`crm` joins the default `RESOURCES_SEARCHED`; tg, max, wa and the calendar CLIs pass their own lists and are unchanged.
|
|
20
|
+
|
|
21
|
+
### Changed — may break callers
|
|
22
|
+
|
|
23
|
+
- **Project status money comes only from the books.** `ProjectStatus.money.open` sums what confirmed open
|
|
24
|
+
invoices still owe and never an amount found by words in messages, so a project with no confirmed invoices answers
|
|
25
|
+
`money: []`. When the project or a child has a confirmed invoice or payment, the `amounts` signal (version 2) lists
|
|
26
|
+
nothing: its word match also found the owner's own quotes, product prices and deposits and called them open, about
|
|
27
|
+
50 of them on one real project whose books say nothing is owed. A project with no books still gets its `amounts`
|
|
28
|
+
items, marked `basis: pattern`. `StatusContext` has a new required `books` flag.
|
|
29
|
+
|
|
10
30
|
## 0.588.0 — 06.11.2026
|
|
11
31
|
|
|
12
32
|
### Changed — may break callers
|
|
@@ -280,6 +280,9 @@ const ALL_HIT_UNTRUSTED = {
|
|
|
280
280
|
note: undefined,
|
|
281
281
|
event: ["title", "excerpt"],
|
|
282
282
|
meeting: ["title", "excerpt", "sender_name"],
|
|
283
|
+
// A CRM's copy is typed by the CRM's users; the owner's own deal is his, but one rule keeps a hit's marking simple.
|
|
284
|
+
deal: ["title", "excerpt"],
|
|
285
|
+
lead: ["title", "excerpt"],
|
|
283
286
|
};
|
|
284
287
|
export const allHitItem = (item) => item.kind === "meeting"
|
|
285
288
|
? {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Provider } from "./models.js";
|
|
2
2
|
import { type RefKindName } from "./references.js";
|
|
3
3
|
/** What a search reads; the same names `--only`, `searched` and `skipped` use. */
|
|
4
|
-
export declare const KINDS: readonly ["messages", "mail", "notes", "documents", "meetings", "events"];
|
|
4
|
+
export declare const KINDS: readonly ["messages", "mail", "notes", "documents", "meetings", "events", "crm"];
|
|
5
5
|
export type Kind = (typeof KINDS)[number];
|
|
6
6
|
export interface ProviderEntry {
|
|
7
7
|
name: Provider;
|
|
@@ -12,6 +12,12 @@ export interface ProviderEntry {
|
|
|
12
12
|
/** The read command, after `<command> <profile>`, that opens a ref of each kind: `{ chat: "messages list" }`. */
|
|
13
13
|
opens?: Partial<Record<RefKindName, string>>;
|
|
14
14
|
}
|
|
15
|
+
/** What a CRM CLI's entry gives as `opens`: the read commands `addCrmCommands` mounts. */
|
|
16
|
+
export declare const CRM_OPENS: {
|
|
17
|
+
readonly deal: "deals show";
|
|
18
|
+
readonly lead: "leads show";
|
|
19
|
+
readonly pipeline: "pipelines show";
|
|
20
|
+
};
|
|
15
21
|
/** The seed: what cli-core knows before any CLI has written its entry into the store. */
|
|
16
22
|
export declare const PROVIDERS: readonly ProviderEntry[];
|
|
17
23
|
/** How a text names a provider to a person: `MAX`, not `max`. */
|
package/dist/domain/providers.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { REF_KINDS } from "./references.js";
|
|
3
3
|
/** What a search reads; the same names `--only`, `searched` and `skipped` use. */
|
|
4
|
-
export const KINDS = ["messages", "mail", "notes", "documents", "meetings", "events"];
|
|
4
|
+
export const KINDS = ["messages", "mail", "notes", "documents", "meetings", "events", "crm"];
|
|
5
5
|
const MESSENGER_OPENS = { chat: "messages list", message: "messages show", contact: "contacts show" };
|
|
6
|
+
/** What a CRM CLI's entry gives as `opens`: the read commands `addCrmCommands` mounts. */
|
|
7
|
+
export const CRM_OPENS = { deal: "deals show", lead: "leads show", pipeline: "pipelines show" };
|
|
6
8
|
/** The seed: what cli-core knows before any CLI has written its entry into the store. */
|
|
7
9
|
export const PROVIDERS = [
|
|
8
10
|
{ name: "telegram", kind: "messages", aliases: ["tg"], command: "tg", opens: MESSENGER_OPENS },
|
package/dist/entities/hit.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
|
-
/** One result of `search all`: a message, a mail, a note, a calendar event
|
|
2
|
+
/** One result of `search all`: a message, a mail, a note, a calendar event, a meeting passage, a deal or a lead. */
|
|
3
3
|
export declare const hit: v.StrictObjectSchema<{
|
|
4
|
-
readonly kind: v.PicklistSchema<["message", "mail", "note", "document", "event", "meeting"], undefined>;
|
|
4
|
+
readonly kind: v.PicklistSchema<["message", "mail", "note", "document", "event", "meeting", "deal", "lead"], undefined>;
|
|
5
5
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
6
6
|
readonly provider: v.StringSchema<undefined>;
|
|
7
7
|
readonly url: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
package/dist/entities/hit.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
2
|
import { hitFile } from "./attachment.js";
|
|
3
3
|
import { count, reference, text, time, untrusted } from "./fields.js";
|
|
4
|
-
/** One result of `search all`: a message, a mail, a note, a calendar event
|
|
4
|
+
/** One result of `search all`: a message, a mail, a note, a calendar event, a meeting passage, a deal or a lead. */
|
|
5
5
|
export const hit = v.strictObject({
|
|
6
|
-
kind: v.picklist(["message", "mail", "note", "document", "event", "meeting"]),
|
|
6
|
+
kind: v.picklist(["message", "mail", "note", "document", "event", "meeting", "deal", "lead"]),
|
|
7
7
|
ref: reference,
|
|
8
8
|
provider: text,
|
|
9
9
|
url: v.optional(text),
|
package/dist/entities/index.d.ts
CHANGED
|
@@ -4065,7 +4065,7 @@ export declare const ENTITIES: {
|
|
|
4065
4065
|
readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
4066
4066
|
}, undefined>;
|
|
4067
4067
|
readonly hit: v.StrictObjectSchema<{
|
|
4068
|
-
readonly kind: v.PicklistSchema<["message", "mail", "note", "document", "event", "meeting"], undefined>;
|
|
4068
|
+
readonly kind: v.PicklistSchema<["message", "mail", "note", "document", "event", "meeting", "deal", "lead"], undefined>;
|
|
4069
4069
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
4070
4070
|
readonly provider: v.StringSchema<undefined>;
|
|
4071
4071
|
readonly url: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type * from "./domain/models.js";
|
|
|
12
12
|
export { CURRENCY_SCALES, currencyOf, type Money, minorUnits, moneyLabel, moneyOf, } from "./domain/money.js";
|
|
13
13
|
export { ORDER_PURPOSES, ORDER_STATUSES, type OrderPurpose, type OrderStatus, } from "./domain/orders.js";
|
|
14
14
|
export { countedPoll } from "./domain/polls.js";
|
|
15
|
-
export { FOLDER_PROVIDERS, isFolderProvider, isKind, KINDS, type Kind, kindIn, kindOf, PROVIDERS, type ProviderEntry, providerNamed, providers, UNKNOWN_PROVIDER, useProviders, } from "./domain/providers.js";
|
|
15
|
+
export { CRM_OPENS, FOLDER_PROVIDERS, isFolderProvider, isKind, KINDS, type Kind, kindIn, kindOf, PROVIDERS, type ProviderEntry, providerNamed, providers, UNKNOWN_PROVIDER, useProviders, } from "./domain/providers.js";
|
|
16
16
|
export type { RankingGraphEvidence, RankingGraphLink } from "./domain/rankings-graph.js";
|
|
17
17
|
export type { RankingComponent, RankingInput, RankingMeasure, RankingOptions, RankingTarget, RankingWeights, ScorePreset, } from "./domain/rankings-options.js";
|
|
18
18
|
export { rankingOptions } from "./domain/rankings-options.js";
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export { parseMarkdownSpans } from "./domain/markdown-spans.js";
|
|
|
9
9
|
export { CURRENCY_SCALES, currencyOf, minorUnits, moneyLabel, moneyOf, } from "./domain/money.js";
|
|
10
10
|
export { ORDER_PURPOSES, ORDER_STATUSES, } from "./domain/orders.js";
|
|
11
11
|
export { countedPoll } from "./domain/polls.js";
|
|
12
|
-
export { FOLDER_PROVIDERS, isFolderProvider, isKind, KINDS, kindIn, kindOf, PROVIDERS, providerNamed, providers, UNKNOWN_PROVIDER, useProviders, } from "./domain/providers.js";
|
|
12
|
+
export { CRM_OPENS, FOLDER_PROVIDERS, isFolderProvider, isKind, KINDS, kindIn, kindOf, PROVIDERS, providerNamed, providers, UNKNOWN_PROVIDER, useProviders, } from "./domain/providers.js";
|
|
13
13
|
export { rankingOptions } from "./domain/rankings-options.js";
|
|
14
14
|
export { envelopeOfJsonl, RECORD_ENTITY_TYPES, RECORD_OPERATIONS, RECORDS_ENVELOPE_SCHEMA, RECORDS_SCHEMA_VERSION, } from "./domain/records.js";
|
|
15
15
|
export { canonicalReference, formatReference, looksLikeReference, parseReference, REF_KINDS, } from "./domain/references.js";
|
|
@@ -203,6 +203,7 @@ const READS = {
|
|
|
203
203
|
documents: "documents — search notes reads them",
|
|
204
204
|
meetings: "meetings — search all reads them",
|
|
205
205
|
events: "calendar events — no search reads them yet",
|
|
206
|
+
crm: "CRM deals and leads — search all reads them",
|
|
206
207
|
};
|
|
207
208
|
/**
|
|
208
209
|
* The accounts of `kinds` the entries name, and each entry that names none of them with the reason. A search
|
|
@@ -60,7 +60,7 @@ export interface ProjectStatus {
|
|
|
60
60
|
chats: StatusChatSummary[];
|
|
61
61
|
/** Every signal's items, in signal order, then chat order. */
|
|
62
62
|
items: StatusItem[];
|
|
63
|
-
/**
|
|
63
|
+
/** What confirmed open invoices still owe, summed per direction and currency; never an amount found in words. */
|
|
64
64
|
money: {
|
|
65
65
|
open: MoneyTotal[];
|
|
66
66
|
};
|
|
@@ -53,23 +53,23 @@ const childrenOf = async (store, root, catalog) => {
|
|
|
53
53
|
};
|
|
54
54
|
const inAccounts = (accounts, provider, account) => accounts === undefined || accounts.some((one) => one.provider === provider && one.account === account);
|
|
55
55
|
const failure = (error) => (error instanceof Error ? error.message : String(error));
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
56
|
+
const owed = (item) => {
|
|
57
|
+
const data = item.data;
|
|
58
|
+
return item.signal === "payments" && data.state === "expected" && data.status === "confirmed";
|
|
59
|
+
};
|
|
60
|
+
const keepsBooks = async (store, projects) => {
|
|
61
|
+
for (const project of projects) {
|
|
62
|
+
const filter = { project, review: "confirmed", limit: 1 };
|
|
63
|
+
if ((await store.finance.invoices(filter)).length || (await store.finance.payments(filter)).length)
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
66
67
|
};
|
|
67
68
|
const totalsOf = (items) => {
|
|
68
69
|
const totals = new Map();
|
|
69
|
-
const open = counted(items);
|
|
70
70
|
for (const item of items) {
|
|
71
71
|
const data = item.data;
|
|
72
|
-
if (!
|
|
72
|
+
if (!owed(item) || data.amount === undefined)
|
|
73
73
|
continue;
|
|
74
74
|
const key = `${data.direction}|${data.currency ?? ""}`;
|
|
75
75
|
const total = totals.get(key) ?? {
|
|
@@ -157,6 +157,7 @@ export const projectStatus = async (store, input) => {
|
|
|
157
157
|
chats,
|
|
158
158
|
project: { ...root, children, cut },
|
|
159
159
|
store,
|
|
160
|
+
books: await keepsBooks(store, [root.ref, ...children.map((child) => child.ref)]),
|
|
160
161
|
contextQueries: input.contextQueries ?? [],
|
|
161
162
|
};
|
|
162
163
|
const items = [];
|
|
@@ -16,5 +16,6 @@ export declare const centsText: (cents: number) => string;
|
|
|
16
16
|
/**
|
|
17
17
|
* Every message with an amount and a money word, folded by amount and currency per chat: the first ask is `since`, a
|
|
18
18
|
* later "paid" message naming the same amount makes it `paid-mentioned`. Instalments are not summed into a total.
|
|
19
|
+
* Nothing when the project keeps books: a word match also finds the owner's quotes and prices, which owe nothing.
|
|
19
20
|
*/
|
|
20
21
|
export declare const amounts: StatusSignal<AmountsData>;
|
|
@@ -47,13 +47,14 @@ export const centsText = (cents) => `${Math.floor(cents / 100)}.${String(cents %
|
|
|
47
47
|
/**
|
|
48
48
|
* Every message with an amount and a money word, folded by amount and currency per chat: the first ask is `since`, a
|
|
49
49
|
* later "paid" message naming the same amount makes it `paid-mentioned`. Instalments are not summed into a total.
|
|
50
|
+
* Nothing when the project keeps books: a word match also finds the owner's quotes and prices, which owe nothing.
|
|
50
51
|
*/
|
|
51
52
|
export const amounts = {
|
|
52
53
|
id: "amounts",
|
|
53
|
-
version:
|
|
54
|
+
version: 2,
|
|
54
55
|
scope: "chat",
|
|
55
|
-
read: async (
|
|
56
|
-
if (!chat)
|
|
56
|
+
read: async (context, chat) => {
|
|
57
|
+
if (!chat || context.books)
|
|
57
58
|
return [];
|
|
58
59
|
const groups = new Map();
|
|
59
60
|
for (const row of rowsOf(chat)) {
|
|
@@ -43,6 +43,8 @@ export interface StatusContext {
|
|
|
43
43
|
};
|
|
44
44
|
/** Read only: facts, tasks, chats. */
|
|
45
45
|
store: MessageStore;
|
|
46
|
+
/** The project or a child has a confirmed invoice or payment: money is then read from those rows, never from words. */
|
|
47
|
+
books: boolean;
|
|
46
48
|
/** `--context-query` texts: every word of one must appear in one message. */
|
|
47
49
|
contextQueries: string[];
|
|
48
50
|
}
|
|
@@ -3,7 +3,7 @@ import type { AccountKey, MessageStore } from "../store/store.js";
|
|
|
3
3
|
import type { SearchFound, SearchQuery } from "./messages.js";
|
|
4
4
|
import type { MeaningLane } from "./messages-discovery.js";
|
|
5
5
|
import { type SearchAllFound, type SearchAllItem, type SearchAllMessenger, type SearchAllRequest, type Skipped } from "./search-all.js";
|
|
6
|
-
export declare const RESOURCES_SEARCHED_WITH_MEETINGS: readonly ["messages", "mail", "notes", "events", "meetings"];
|
|
6
|
+
export declare const RESOURCES_SEARCHED_WITH_MEETINGS: readonly ["messages", "mail", "notes", "events", "crm", "meetings"];
|
|
7
7
|
export type ResourceWithMeetings = (typeof RESOURCES_SEARCHED_WITH_MEETINGS)[number];
|
|
8
8
|
export interface MeetingSearchItem {
|
|
9
9
|
kind: "meeting";
|
|
@@ -274,6 +274,7 @@ const searchAllOfSource = async (store, account, source, request, messenger, sea
|
|
|
274
274
|
mail: named("mail"),
|
|
275
275
|
notes: named("notes"),
|
|
276
276
|
events: named("events"),
|
|
277
|
+
crm: named("crm"),
|
|
277
278
|
},
|
|
278
279
|
...(meetings[0] ? { meetingAccount: meetings[0] } : {}),
|
|
279
280
|
}, messenger, searchMessages, meaning);
|
|
@@ -7,11 +7,11 @@ import type { SearchFound, SearchQuery } from "./messages.js";
|
|
|
7
7
|
import { type MeaningLane, type MeaningSkipped } from "./messages-discovery.js";
|
|
8
8
|
import type { SearchCoverage } from "./messages-search.js";
|
|
9
9
|
import type { Backend, ServerOptions, ServerSearched } from "./server-search.js";
|
|
10
|
-
export declare const RESOURCES_SEARCHED: readonly ["messages", "mail", "notes", "events"];
|
|
10
|
+
export declare const RESOURCES_SEARCHED: readonly ["messages", "mail", "notes", "events", "crm"];
|
|
11
11
|
export type SearchedResource = (typeof RESOURCES_SEARCHED)[number];
|
|
12
12
|
export interface SearchAllItem {
|
|
13
|
-
kind: "message" | "mail" | "note" | "document" | "event";
|
|
14
|
-
/** `msg:…` for a message or a mail, `note:…` for a note, `event:…` for a calendar event
|
|
13
|
+
kind: "message" | "mail" | "note" | "document" | "event" | "deal" | "lead";
|
|
14
|
+
/** `msg:…` for a message or a mail, `note:…` for a note, `event:…` for a calendar event, `deal:…`, `lead:…`. */
|
|
15
15
|
ref: string;
|
|
16
16
|
provider: string;
|
|
17
17
|
/** The account it was found in; a note's folder id, or `internal` for a note written in memo. */
|
|
@@ -97,7 +97,7 @@ export interface SearchAllRequest {
|
|
|
97
97
|
only?: readonly SearchedResource[];
|
|
98
98
|
exact?: boolean;
|
|
99
99
|
timezone?: string;
|
|
100
|
-
/** For messages only; mail, notes and
|
|
100
|
+
/** For messages only; mail, notes, events and CRM copies are always the local store's. */
|
|
101
101
|
backend?: Backend;
|
|
102
102
|
server?: ServerOptions;
|
|
103
103
|
env?: NodeJS.ProcessEnv;
|
|
@@ -113,7 +113,17 @@ export type SearchAllMessenger = Partial<Pick<MessengerTraits, "savedChatId" | "
|
|
|
113
113
|
hintCommand?: string | null;
|
|
114
114
|
};
|
|
115
115
|
/**
|
|
116
|
-
*
|
|
116
|
+
* What a deal or a lead is found by: words or phrases of its title, all of them, or any with OR. Anything else is
|
|
117
|
+
* refused, so `search all` skips deals with the reason instead of matching a field they do not have.
|
|
118
|
+
*/
|
|
119
|
+
export declare const titleWords: (text: string) => {
|
|
120
|
+
words: string[];
|
|
121
|
+
anyWord: boolean;
|
|
122
|
+
};
|
|
123
|
+
/** The accounts of every CRM the store holds copies of. */
|
|
124
|
+
export declare const crmAccounts: (store: MessageStore) => Promise<AccountKey[]>;
|
|
125
|
+
/**
|
|
126
|
+
* `search all`: messages, mail, notes, calendar events and CRM deals and leads from the local store — messages also from the messenger's
|
|
117
127
|
* server, as `search messages` asks it — each list best first, merged by reciprocal rank so no
|
|
118
128
|
* resource's own scores have to be compared with another's. Events are read only when the store holds a calendar.
|
|
119
129
|
*/
|
|
@@ -6,14 +6,14 @@ import { senderIsMe } from "../domain/models.js";
|
|
|
6
6
|
import { INTERNAL_NOTES, kindIn } from "../domain/providers.js";
|
|
7
7
|
import { runEmbedders } from "../embeddings/embed.js";
|
|
8
8
|
import { DEFAULT_TEXT_MODEL } from "../embeddings/models.js";
|
|
9
|
-
import { MESSAGE_PARSING } from "../search/lucene/types.js";
|
|
9
|
+
import { MESSAGE_PARSING, SEARCH_QUERY } from "../search/lucene/types.js";
|
|
10
10
|
import { planQuestion, questionLookup } from "../search/question-plan.js";
|
|
11
11
|
import { calendarAccounts, searchEvents } from "./events-search.js";
|
|
12
12
|
import { excerpt } from "./excerpt.js";
|
|
13
13
|
import { MAX_SEARCH_OFFSET, searchStore } from "./messages-archive-search.js";
|
|
14
14
|
import { nearEligible } from "./messages-discovery.js";
|
|
15
15
|
import { notesScopeOf, RRF_K, searchNotes } from "./notes-search.js";
|
|
16
|
-
export const RESOURCES_SEARCHED = ["messages", "mail", "notes", "events"];
|
|
16
|
+
export const RESOURCES_SEARCHED = ["messages", "mail", "notes", "events", "crm"];
|
|
17
17
|
/** A query a resource cannot answer is skipped with its reason, never a failure of the whole search. */
|
|
18
18
|
const skippable = (error) => error instanceof CliError && (error.code === "validation_error" || error.code === "not_found");
|
|
19
19
|
/**
|
|
@@ -31,7 +31,26 @@ const filtersBeside = (text) => {
|
|
|
31
31
|
: undefined;
|
|
32
32
|
};
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* What a deal or a lead is found by: words or phrases of its title, all of them, or any with OR. Anything else is
|
|
35
|
+
* refused, so `search all` skips deals with the reason instead of matching a field they do not have.
|
|
36
|
+
*/
|
|
37
|
+
export const titleWords = (text) => {
|
|
38
|
+
const { root } = parseLucene(text, SEARCH_QUERY);
|
|
39
|
+
const clauses = root.kind === "boolean" ? root.clauses : [{ node: root, occur: "must" }];
|
|
40
|
+
const words = clauses.map(({ node, occur }) => occur !== "must_not" &&
|
|
41
|
+
node.kind === "predicate" &&
|
|
42
|
+
node.field === "text" &&
|
|
43
|
+
(node.operator === "term" || node.operator === "phrase")
|
|
44
|
+
? node.value
|
|
45
|
+
: (() => {
|
|
46
|
+
throw new CliError("validation_error", "deals and leads are found by the words of their title only");
|
|
47
|
+
})());
|
|
48
|
+
return { words, anyWord: clauses.length > 1 && clauses.every(({ occur }) => occur === "should") };
|
|
49
|
+
};
|
|
50
|
+
/** The accounts of every CRM the store holds copies of. */
|
|
51
|
+
export const crmAccounts = async (store) => (await store.accounts.accounts()).filter(({ provider }) => kindIn(provider) === "crm");
|
|
52
|
+
/**
|
|
53
|
+
* `search all`: messages, mail, notes, calendar events and CRM deals and leads from the local store — messages also from the messenger's
|
|
35
54
|
* server, as `search messages` asks it — each list best first, merged by reciprocal rank so no
|
|
36
55
|
* resource's own scores have to be compared with another's. Events are read only when the store holds a calendar.
|
|
37
56
|
*/
|
|
@@ -232,6 +251,30 @@ export const searchAll = async (store, account, request, messenger = {}, searchM
|
|
|
232
251
|
})));
|
|
233
252
|
accounts.push(...read.map(({ provider, account }) => ({ resource, provider, account })));
|
|
234
253
|
}
|
|
254
|
+
else if (resource === "crm") {
|
|
255
|
+
const { words, anyWord } = titleWords(request.text);
|
|
256
|
+
const chosen = request.scope?.crm;
|
|
257
|
+
const found = await store.deals.find({
|
|
258
|
+
words,
|
|
259
|
+
anyWord: anyWord || discover,
|
|
260
|
+
limit: Math.min(window + 1, 500),
|
|
261
|
+
...(chosen === undefined ? {} : { accounts: chosen }),
|
|
262
|
+
});
|
|
263
|
+
hasMore ||= found.length > window;
|
|
264
|
+
lists.push(found.slice(0, window).map((deal) => ({
|
|
265
|
+
kind: deal.kind,
|
|
266
|
+
ref: deal.ref,
|
|
267
|
+
provider: deal.account?.provider ?? "crm",
|
|
268
|
+
account: deal.account?.account ?? INTERNAL_NOTES,
|
|
269
|
+
title: deal.title,
|
|
270
|
+
timestamp: deal.updatedAt,
|
|
271
|
+
text: deal.summary,
|
|
272
|
+
foundBy: ["title"],
|
|
273
|
+
})));
|
|
274
|
+
const read = chosen ?? [...(await crmAccounts(store)), { provider: "crm", account: INTERNAL_NOTES }];
|
|
275
|
+
for (const { provider, account } of read)
|
|
276
|
+
accounts.push({ resource, provider, account });
|
|
277
|
+
}
|
|
235
278
|
else
|
|
236
279
|
lists.push(await messagesOf(resource));
|
|
237
280
|
searched.push(resource);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type DealKind, type DealRole, type DealStatus, type StageOutcome } from "../../domain/deals.js";
|
|
2
|
+
import type { AccountKey } from "../types.js";
|
|
2
3
|
import { type ProposedByFilter } from "./actors.js";
|
|
3
4
|
import { type Party } from "./finance.js";
|
|
4
5
|
import { type Author } from "./memories.js";
|
|
@@ -134,6 +135,16 @@ export interface DealFilter {
|
|
|
134
135
|
/** 1–500, 100 by default; newest written first. */
|
|
135
136
|
limit?: number;
|
|
136
137
|
}
|
|
138
|
+
export interface DealSearch {
|
|
139
|
+
/** Words of the title, each in any letter case; a word may be a phrase. */
|
|
140
|
+
words: string[];
|
|
141
|
+
/** A title holding any of the words matches, those holding more first; otherwise every word is needed. */
|
|
142
|
+
anyWord?: boolean;
|
|
143
|
+
/** Only the copies of these CRM accounts; absent, every account's and the owner's own. */
|
|
144
|
+
accounts?: AccountKey[];
|
|
145
|
+
/** 1–500, 100 by default. */
|
|
146
|
+
limit?: number;
|
|
147
|
+
}
|
|
137
148
|
export interface PipelineInput {
|
|
138
149
|
name: string;
|
|
139
150
|
/** `deal` when left out. */
|
|
@@ -164,6 +175,8 @@ export interface DealsStore {
|
|
|
164
175
|
/** `deal:<id>` or `lead:<id>`; a bare id is a deal. */
|
|
165
176
|
get(reference: string): Promise<Deal>;
|
|
166
177
|
list(filter?: DealFilter): Promise<Deal[]>;
|
|
178
|
+
/** Deals and leads together, by the words of their title: the most words matched first, then the latest changed. */
|
|
179
|
+
find(search: DealSearch): Promise<Deal[]>;
|
|
167
180
|
/** To another stage of its own pipeline; a stage whose outcome is won or lost closes it, an open one reopens it. */
|
|
168
181
|
move(reference: string, input: {
|
|
169
182
|
stage: string;
|
|
@@ -189,6 +202,7 @@ export declare const dealOperationsOver: (context: StoreContext) => {
|
|
|
189
202
|
add: (input: DealInput) => Deal;
|
|
190
203
|
get: (reference: string) => Deal;
|
|
191
204
|
list: (filter?: DealFilter) => Deal[];
|
|
205
|
+
find: ({ words, anyWord, accounts, limit }: DealSearch) => Deal[];
|
|
192
206
|
move: (reference: string, { stage, by }: {
|
|
193
207
|
stage: string;
|
|
194
208
|
by: Author;
|
|
@@ -334,6 +334,30 @@ export const dealOperationsOver = (context) => {
|
|
|
334
334
|
.all(status, status, project, project, stage, stage, pipeline, pipeline, organization?.id ?? null, organization?.id ?? null, ...by.params, limit)
|
|
335
335
|
.map(itemOf(kind));
|
|
336
336
|
},
|
|
337
|
+
find: ({ words, anyWord, accounts, limit }) => {
|
|
338
|
+
const wanted = [...new Set(words.map((word) => fold(word).trim()).filter(Boolean))];
|
|
339
|
+
if (!wanted.length)
|
|
340
|
+
return [];
|
|
341
|
+
const most = Math.min(Math.max(limit ?? 100, 1), 500);
|
|
342
|
+
const within = accounts?.map(({ provider, account }) => `${provider}\u0000${account}`);
|
|
343
|
+
// Titles are short and a CRM holds thousands, not millions: folding them here is cheaper than a word index.
|
|
344
|
+
const found = DEAL_KINDS.flatMap((kind) => database
|
|
345
|
+
.prepare(`SELECT d.id, d.title, d.updated_at, a.provider, a.external_id AS account FROM ${TABLES[kind]} d ` +
|
|
346
|
+
"LEFT JOIN accounts a ON a.id = d.account_id WHERE d.deleted_at IS NULL")
|
|
347
|
+
.all()
|
|
348
|
+
.filter((row) => within === undefined || within.includes(`${row.provider}\u0000${row.account}`))
|
|
349
|
+
.map((row) => {
|
|
350
|
+
const title = fold(String(row.title));
|
|
351
|
+
return { kind, row, score: wanted.filter((word) => title.includes(word)).length };
|
|
352
|
+
})
|
|
353
|
+
.filter(({ score }) => (anyWord ? score > 0 : score === wanted.length)));
|
|
354
|
+
return found
|
|
355
|
+
.sort((a, b) => b.score - a.score ||
|
|
356
|
+
Number(b.row.updated_at) - Number(a.row.updated_at) ||
|
|
357
|
+
Number(b.row.id) - Number(a.row.id))
|
|
358
|
+
.slice(0, most)
|
|
359
|
+
.map(({ kind, row }) => read(`${kind}:${row.id}`));
|
|
360
|
+
},
|
|
337
361
|
move: (reference, { stage, by }) => {
|
|
338
362
|
authorActor(context, by);
|
|
339
363
|
const { kind, row } = rowOf(reference);
|
|
@@ -372,6 +396,7 @@ export const dealsStoreOver = (context) => {
|
|
|
372
396
|
add: async (input) => operations.add(input),
|
|
373
397
|
get: async (reference) => operations.get(reference),
|
|
374
398
|
list: async (filter) => operations.list(filter),
|
|
399
|
+
find: async (search) => operations.find(search),
|
|
375
400
|
move: async (reference, input) => operations.move(reference, input),
|
|
376
401
|
close: async (reference, input) => operations.close(reference, input),
|
|
377
402
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.589.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",
|