@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/cli/messenger/search-answers.js +3 -0
  3. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  4. package/dist/domain/deals.d.ts +8 -6
  5. package/dist/domain/deals.js +7 -8
  6. package/dist/domain/providers.d.ts +7 -1
  7. package/dist/domain/providers.js +3 -1
  8. package/dist/domain/ref-summaries.js +2 -0
  9. package/dist/domain/references.d.ts +1 -1
  10. package/dist/domain/references.js +2 -0
  11. package/dist/entities/hit.d.ts +2 -2
  12. package/dist/entities/hit.js +2 -2
  13. package/dist/entities/index.d.ts +1 -1
  14. package/dist/errors/reasons.js +6 -6
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +2 -2
  17. package/dist/search/account-list.js +1 -0
  18. package/dist/services/project-status/index.d.ts +1 -1
  19. package/dist/services/project-status/index.js +13 -12
  20. package/dist/services/project-status/signals/amounts.d.ts +1 -0
  21. package/dist/services/project-status/signals/amounts.js +4 -3
  22. package/dist/services/project-status/types.d.ts +2 -0
  23. package/dist/services/proposals/registry.js +0 -2
  24. package/dist/services/search-all-meetings.d.ts +1 -1
  25. package/dist/services/search-all-meetings.js +1 -0
  26. package/dist/services/search-all.d.ts +15 -5
  27. package/dist/services/search-all.js +46 -3
  28. package/dist/store/index.d.ts +1 -1
  29. package/dist/store/sqlite/deals.d.ts +142 -29
  30. package/dist/store/sqlite/deals.js +295 -156
  31. package/dist/store/sqlite/entity-types.d.ts +3 -1
  32. package/dist/store/sqlite/entity-types.js +3 -1
  33. package/dist/store/sqlite/knowledge.js +3 -1
  34. package/dist/store/sqlite/migrations.generated.js +39 -11
  35. package/dist/store/sqlite/ref-summaries.js +11 -7
  36. package/dist/store/sqlite/schema.d.ts +600 -38
  37. package/dist/store/sqlite/schema.js +86 -14
  38. package/dist/store/sqlite/things.js +2 -0
  39. package/package.json +1 -1
  40. package/dist/services/proposals/kinds/deals.d.ts +0 -4
  41. 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;
@@ -1,9 +1,14 @@
1
- import type { FinanceReview } from "./finance.js";
2
- /** Whether a deal ended, kept apart from the stage it stands in. */
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;
@@ -1,15 +1,14 @@
1
1
  import { moneyLabel } from "./money.js";
2
- /** Whether a deal ended, kept apart from the stage it stands in. */
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
- const notes = [deal.status === "open" ? null : deal.status, deal.review === "confirmed" ? null : deal.review]
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`. */
@@ -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 },
@@ -53,6 +53,8 @@ const WORDS = {
53
53
  payment: "Payment",
54
54
  order: "Order",
55
55
  deal: "Deal",
56
+ lead: "Lead",
57
+ pipeline: "Pipeline",
56
58
  email: "Email",
57
59
  email_thread: "Email thread",
58
60
  unknown: "Unknown",
@@ -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> {
@@ -20,6 +20,8 @@ export const SIMPLE_REFERENCES = [
20
20
  "payment",
21
21
  "order",
22
22
  "deal",
23
+ "lead",
24
+ "pipeline",
23
25
  "entity",
24
26
  ];
25
27
  const malformed = (text, shape) => new CliError("validation_error", `"${singleLine(text)}" is not a reference — expected ${shape}`, {
@@ -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 or a meeting passage, best first. */
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>;
@@ -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 or a meeting passage, best first. */
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),
@@ -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>;
@@ -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 won, lost, rejected or deleted deal does not move or close again"),
207
- validation("deal.field", "a field of the deal is not valid"),
208
- ["deal.owner-only", "permission_error", "only the owner moves or closes a deal, or makes a pipeline"],
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.duplicate", "a pipeline of that name is already stored"),
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
- /** Open amounts and what confirmed open invoices still owe, summed per direction and currency. */
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
- * An open ask, or a confirmed open invoice; an ask that a confirmed row cites is counted once, as the row. A proposed
58
- * row promises nothing yet, so the ask it cites still counts.
59
- */
60
- const counted = (items) => {
61
- const confirmed = (item) => item.signal === "payments" && item.data.status === "confirmed";
62
- const recorded = new Set(items.filter(confirmed).flatMap((item) => item.evidence.slice(1).map((one) => one.ref)));
63
- return (item, state) => item.signal === "payments"
64
- ? state === "expected" && confirmed(item)
65
- : item.signal === "amounts" && state === "open" && !item.evidence.some((one) => recorded.has(one.ref));
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 (!open(item, data.state) || data.amount === undefined)
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: 1,
54
+ version: 2,
54
55
  scope: "chat",
55
- read: async (_context, chat) => {
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 events are always the local store's. */
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
- * `search all`: messages, mail, notes and calendar events from the local store — messages also from the messenger's
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
- * `search all`: messages, mail, notes and calendar events from the local store — messages also from the messenger's
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);
@@ -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";