@wirecat/cli-core 0.587.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 +42 -0
- package/dist/cli/messenger/search-answers.js +3 -0
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/domain/deals.d.ts +8 -6
- package/dist/domain/deals.js +7 -8
- package/dist/domain/providers.d.ts +7 -1
- package/dist/domain/providers.js +3 -1
- package/dist/domain/ref-summaries.js +2 -0
- package/dist/domain/references.d.ts +1 -1
- package/dist/domain/references.js +2 -0
- 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/errors/reasons.js +6 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- 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/proposals/registry.js +0 -2
- 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/index.d.ts +1 -1
- package/dist/store/sqlite/deals.d.ts +142 -29
- package/dist/store/sqlite/deals.js +295 -156
- package/dist/store/sqlite/entity-types.d.ts +3 -1
- package/dist/store/sqlite/entity-types.js +3 -1
- package/dist/store/sqlite/knowledge.js +3 -1
- package/dist/store/sqlite/migrations.generated.js +39 -11
- package/dist/store/sqlite/ref-summaries.js +11 -7
- package/dist/store/sqlite/schema.d.ts +600 -38
- package/dist/store/sqlite/schema.js +86 -14
- package/dist/store/sqlite/things.js +2 -0
- package/package.json +1 -1
- package/dist/services/proposals/kinds/deals.d.ts +0 -4
- package/dist/services/proposals/kinds/deals.js +0 -32
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,48 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
+
## Unreleased
|
|
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
|
+
|
|
30
|
+
## 0.588.0 — 06.11.2026
|
|
31
|
+
|
|
32
|
+
### Changed — may break callers
|
|
33
|
+
|
|
34
|
+
- **Store version 52 is amended in place: deals and leads in their own tables, a stage history, and no review.**
|
|
35
|
+
0.585.0, 0.586.0 and 0.587.0 shipped an earlier shape of version 52, but no published CLI depends on them, so no real store
|
|
36
|
+
holds it; a store a test or a local build moved to 52 with them must be made again. Tables `deals` and `leads`
|
|
37
|
+
(refs `deal:<id>`, `lead:<id>`) replace `opportunities`; a lead may name the deal it became (`deal_id`). Pipelines
|
|
38
|
+
have a ref, `pipeline:<id>`, belong to the owner or to a CRM account (`account_id`, `external_id`, so two accounts
|
|
39
|
+
may each have "Sales"), and hold deals or leads (`kind`); stages have an `outcome` (`open`, `won`, `lost`) and an
|
|
40
|
+
`external_id`, and positions are not unique. Deals and leads keep the CRM's `account_id`, `external_id` and a board
|
|
41
|
+
`rank`. Triggers write every stage a deal or a lead enters to `pipeline_stage_changes`. The migration seeds one lead
|
|
42
|
+
pipeline, `Leads`, with the stages `new`, `contacted`, `qualified`, `won` and `lost`.
|
|
43
|
+
- **`store.deals` writes for the owner and his agents alike.** The `deal` proposal kind and the review columns are
|
|
44
|
+
gone; only the author is kept. `add`, `list` take `kind` (`deal` by default; a lead may leave out its stage when one
|
|
45
|
+
lead pipeline is stored). A pipeline is named by `pipeline:<id>` or by a name only one pipeline has; a shared name
|
|
46
|
+
is refused with the candidates (`pipeline.ambiguous`). Moving to a stage whose outcome is won or lost closes the row,
|
|
47
|
+
and `close` moves it to its pipeline's stage of that outcome when there is one. `store.deals.pipeline(ref)` reports
|
|
48
|
+
each stage's open rows, how many rows reached it and the conversion to the next stage, with won, lost and win rate.
|
|
49
|
+
|
|
8
50
|
## 0.587.0 — 06.11.2026
|
|
9
51
|
|
|
10
52
|
### Added
|
|
@@ -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
|
? {
|
|
@@ -3,9 +3,9 @@ import type { CacheDatabase } from "../../store/driver.js";
|
|
|
3
3
|
import type { Messenger } from "./context.js";
|
|
4
4
|
/** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
|
|
5
5
|
export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
|
|
6
|
+
error?: undefined;
|
|
6
7
|
path: string;
|
|
7
8
|
exists: boolean;
|
|
8
|
-
error?: undefined;
|
|
9
9
|
} | {
|
|
10
10
|
path: string;
|
|
11
11
|
exists: boolean;
|
package/dist/domain/deals.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/** A pipeline holds deals or leads, never both; a deal and a lead are rows of their own tables. */
|
|
2
|
+
export declare const DEAL_KINDS: readonly ["deal", "lead"];
|
|
3
|
+
/** Whether a deal or a lead ended, kept apart from the stage it stands in. */
|
|
3
4
|
export declare const DEAL_STATUSES: readonly ["open", "won", "lost"];
|
|
5
|
+
/** What reaching a stage means, as a CRM marks it: still open, or won or lost. */
|
|
6
|
+
export declare const STAGE_OUTCOMES: readonly ["open", "won", "lost"];
|
|
4
7
|
/** The part a person plays on a deal: the owner's side runs it, the client's side talks or decides. */
|
|
5
8
|
export declare const DEAL_ROLES: readonly ["owner", "contact", "decision-maker"];
|
|
9
|
+
export type DealKind = (typeof DEAL_KINDS)[number];
|
|
6
10
|
export type DealStatus = (typeof DEAL_STATUSES)[number];
|
|
11
|
+
export type StageOutcome = DealStatus;
|
|
7
12
|
export type DealRole = (typeof DEAL_ROLES)[number];
|
|
8
13
|
export interface DealTitleFields {
|
|
9
14
|
id: number;
|
|
@@ -12,9 +17,6 @@ export interface DealTitleFields {
|
|
|
12
17
|
amount: number | null;
|
|
13
18
|
currency: string | null;
|
|
14
19
|
status: DealStatus;
|
|
15
|
-
review: FinanceReview;
|
|
16
20
|
}
|
|
17
|
-
/** "Site redesign · Proposal, 500 000.00 RUB": a ref summary's title, which `refLabel` prefixes. */
|
|
21
|
+
/** "Site redesign · Proposal, 500 000.00 RUB (won)": a ref summary's title, which `refLabel` prefixes. */
|
|
18
22
|
export declare const dealLine: (deal: DealTitleFields) => string;
|
|
19
|
-
/** "Deal Site redesign · Proposal, 500 000.00 RUB (won)". */
|
|
20
|
-
export declare const dealTitle: (deal: DealTitleFields) => string;
|
package/dist/domain/deals.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { moneyLabel } from "./money.js";
|
|
2
|
-
/**
|
|
2
|
+
/** A pipeline holds deals or leads, never both; a deal and a lead are rows of their own tables. */
|
|
3
|
+
export const DEAL_KINDS = ["deal", "lead"];
|
|
4
|
+
/** Whether a deal or a lead ended, kept apart from the stage it stands in. */
|
|
3
5
|
export const DEAL_STATUSES = ["open", "won", "lost"];
|
|
6
|
+
/** What reaching a stage means, as a CRM marks it: still open, or won or lost. */
|
|
7
|
+
export const STAGE_OUTCOMES = DEAL_STATUSES;
|
|
4
8
|
/** The part a person plays on a deal: the owner's side runs it, the client's side talks or decides. */
|
|
5
9
|
export const DEAL_ROLES = ["owner", "contact", "decision-maker"];
|
|
6
|
-
/** "Site redesign · Proposal, 500 000.00 RUB": a ref summary's title, which `refLabel` prefixes. */
|
|
10
|
+
/** "Site redesign · Proposal, 500 000.00 RUB (won)": a ref summary's title, which `refLabel` prefixes. */
|
|
7
11
|
export const dealLine = (deal) => {
|
|
8
12
|
const money = deal.amount === null || deal.currency === null ? "" : `, ${moneyLabel(deal.amount, deal.currency)}`;
|
|
9
|
-
|
|
10
|
-
.filter(Boolean)
|
|
11
|
-
.join(", ");
|
|
12
|
-
return `${deal.title} · ${deal.stage}${money}${notes ? ` (${notes})` : ""}`;
|
|
13
|
+
return `${deal.title} · ${deal.stage}${money}${deal.status === "open" ? "" : ` (${deal.status})`}`;
|
|
13
14
|
};
|
|
14
|
-
/** "Deal Site redesign · Proposal, 500 000.00 RUB (won)". */
|
|
15
|
-
export const dealTitle = (deal) => `Deal ${dealLine(deal)}`;
|
|
@@ -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 },
|
|
@@ -33,7 +33,7 @@ export type Reference = {
|
|
|
33
33
|
* Things named by their store id (a task also by its key or the task package's id). `entity` is the name
|
|
34
34
|
* before organizations and projects: it still parses, so an old file's reference resolves as not found.
|
|
35
35
|
*/
|
|
36
|
-
export declare const SIMPLE_REFERENCES: readonly ["note", "document", "person", "organization", "project", "task", "memory", "fact", "bot", "event", "invoice", "payment", "order", "deal", "entity"];
|
|
36
|
+
export declare const SIMPLE_REFERENCES: readonly ["note", "document", "person", "organization", "project", "task", "memory", "fact", "bot", "event", "invoice", "payment", "order", "deal", "lead", "pipeline", "entity"];
|
|
37
37
|
export type SimpleReference = (typeof SIMPLE_REFERENCES)[number];
|
|
38
38
|
export type RefKindName = Exclude<Reference["type"], "entity">;
|
|
39
39
|
export interface RefKindSpec<R extends Reference = Reference> {
|
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/errors/reasons.js
CHANGED
|
@@ -203,14 +203,14 @@ const OTHERS = [
|
|
|
203
203
|
validation("money.amount", "the amount is not a decimal string in the currency's scale"),
|
|
204
204
|
validation("money.currency", "the currency is not one the store keeps money in"),
|
|
205
205
|
validation("money.quantity", "the quantity is not a decimal above 0"),
|
|
206
|
-
validation("deal.closed", "a
|
|
207
|
-
validation("deal.field", "a field of the deal is not valid"),
|
|
208
|
-
|
|
209
|
-
validation("deal.ref", "the reference names no deal"),
|
|
206
|
+
validation("deal.closed", "a deleted deal or lead does not move or close"),
|
|
207
|
+
validation("deal.field", "a field of the deal or lead is not valid"),
|
|
208
|
+
validation("deal.ref", "the reference names no deal or lead"),
|
|
210
209
|
validation("deal.stage", "no stage of that name, or a stage name more than one pipeline has"),
|
|
211
|
-
validation("pipeline.
|
|
210
|
+
validation("pipeline.ambiguous", "more than one pipeline has that name; name it by its pipeline: reference"),
|
|
211
|
+
validation("pipeline.duplicate", "a pipeline of that name and kind is already stored"),
|
|
212
212
|
validation("pipeline.field", "a pipeline needs a name and at least one stage, each named once"),
|
|
213
|
-
["pipeline.not-found", "not_found", "no stored pipeline has that name"],
|
|
213
|
+
["pipeline.not-found", "not_found", "no stored pipeline has that name or reference"],
|
|
214
214
|
validation("order.duplicate", "the shop's order with this number is already stored; change its status instead"),
|
|
215
215
|
validation("order.proposed", "the shop's order with this number already waits for the owner in a findings batch"),
|
|
216
216
|
validation("order.field", "a field of the order is not valid"),
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { bareCheck, type CheckWords, checkWords } from "./domain/checks.js";
|
|
2
2
|
export type { CounterField, CounterObservation, CounterObservations, CounterState } from "./domain/counters.js";
|
|
3
3
|
export { COUNTER_FIELDS, counterValue, observedCounters } from "./domain/counters.js";
|
|
4
|
-
export { DEAL_ROLES, DEAL_STATUSES, type DealRole, type DealStatus } from "./domain/deals.js";
|
|
4
|
+
export { DEAL_KINDS, DEAL_ROLES, DEAL_STATUSES, type DealKind, type DealRole, type DealStatus, STAGE_OUTCOMES, type StageOutcome, } from "./domain/deals.js";
|
|
5
5
|
export { FINANCE_REVIEWS, type FinanceReview, INVOICE_DIRECTIONS, INVOICE_STATUSES, type InvoiceDirection, type InvoiceStatus, MONEY_DIRECTIONS, type MoneyDirection, } from "./domain/finance.js";
|
|
6
6
|
export { type FormattedText, type HtmlFormatting, type MarkdownFormatting, type TextSpan, validateFormattedText, } from "./domain/formatting.js";
|
|
7
7
|
export { formatLocator, isLocator, type MessageLocator, parseLocator } from "./domain/locator.js";
|
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { bareCheck, checkWords } from "./domain/checks.js";
|
|
2
2
|
export { COUNTER_FIELDS, counterValue, observedCounters } from "./domain/counters.js";
|
|
3
|
-
export { DEAL_ROLES, DEAL_STATUSES } from "./domain/deals.js";
|
|
3
|
+
export { DEAL_KINDS, DEAL_ROLES, DEAL_STATUSES, STAGE_OUTCOMES, } from "./domain/deals.js";
|
|
4
4
|
export { FINANCE_REVIEWS, INVOICE_DIRECTIONS, INVOICE_STATUSES, MONEY_DIRECTIONS, } from "./domain/finance.js";
|
|
5
5
|
export { validateFormattedText, } from "./domain/formatting.js";
|
|
6
6
|
export { formatLocator, isLocator, parseLocator } from "./domain/locator.js";
|
|
@@ -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
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { mayDecide } from "../../store/decisions.js";
|
|
3
3
|
import { actionKind } from "./kinds/action.js";
|
|
4
|
-
import { dealKind } from "./kinds/deals.js";
|
|
5
4
|
import { factKind } from "./kinds/fact.js";
|
|
6
5
|
import { invoiceKind, paymentKind } from "./kinds/finance.js";
|
|
7
6
|
import { findingsKind } from "./kinds/findings.js";
|
|
@@ -136,7 +135,6 @@ export const proposalRegistry = (store) => {
|
|
|
136
135
|
invoiceKind(store),
|
|
137
136
|
paymentKind(store),
|
|
138
137
|
orderKind(store),
|
|
139
|
-
dealKind(store),
|
|
140
138
|
organizationKind(store),
|
|
141
139
|
memoryKind(store),
|
|
142
140
|
findingsKind(store),
|
|
@@ -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);
|
package/dist/store/index.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export type { BotUpdate, BotUpdateStore } from "./sqlite/bot-updates.js";
|
|
|
34
34
|
export type { CatalogActor, CatalogEntry, CatalogInput, CatalogQuery, CatalogStore } from "./sqlite/catalog.js";
|
|
35
35
|
export { CATALOG_QUERY } from "./sqlite/catalog.js";
|
|
36
36
|
export type { ChatMetadata } from "./sqlite/chats.js";
|
|
37
|
-
export type { Deal, DealFilter, DealInput, DealPerson, DealsStore, Pipeline, PipelineStage, } from "./sqlite/deals.js";
|
|
37
|
+
export type { Deal, DealFilter, DealInput, DealPerson, DealsStore, FunnelStage, Pipeline, PipelineInput, PipelineReport, PipelineStage, } from "./sqlite/deals.js";
|
|
38
38
|
export type { Email, EmailAddress, EmailAttachment, EmailFacts, EmailFilter, EmailInput, EmailInThread, EmailLocationInput, EmailRecipient, EmailRef, EmailThread, EmailVectorHit, FolderEmail, Mailbox, MailboxInput, MailFilter, MailStore, PartialEmail, SenderScore, ThreadDetails, ThreadSave, } from "./sqlite/emails.js";
|
|
39
39
|
export { EVENT_FIELDS, type EventChunkToEmbed, type EventHit, type EventQuery, type EventSearch, type NearestEvent, } from "./sqlite/event-search.js";
|
|
40
40
|
export type { EvidenceSource } from "./sqlite/evidence-sources.js";
|