@wirecat/cli-core 0.590.0 → 0.592.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 (61) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/background/deliver.d.ts +58 -0
  3. package/dist/background/deliver.js +125 -0
  4. package/dist/background/schedules.d.ts +20 -1
  5. package/dist/background/schedules.js +48 -5
  6. package/dist/background/system.d.ts +9 -2
  7. package/dist/background/system.js +10 -3
  8. package/dist/calendar/mcp/index.d.ts +12 -12
  9. package/dist/cli/messenger/flooded.d.ts +1 -1
  10. package/dist/cli/messenger/flooded.js +3 -26
  11. package/dist/cli/messenger/search-command.d.ts +3 -2
  12. package/dist/cli/messenger/search-command.js +7 -5
  13. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  14. package/dist/cli/schedule/schedule-command.js +35 -12
  15. package/dist/crm/cli/index.d.ts +57 -0
  16. package/dist/crm/cli/index.js +300 -0
  17. package/dist/crm/cli/search-all.d.ts +12 -0
  18. package/dist/crm/cli/search-all.js +63 -0
  19. package/dist/crm/index.d.ts +2 -0
  20. package/dist/crm/index.js +1 -0
  21. package/dist/crm/source.d.ts +43 -0
  22. package/dist/crm/source.js +1 -0
  23. package/dist/crm/sync.d.ts +121 -0
  24. package/dist/crm/sync.js +291 -0
  25. package/dist/crm/testing/index.d.ts +3 -0
  26. package/dist/crm/testing/index.js +3 -0
  27. package/dist/crm/testing/source-contract.d.ts +17 -0
  28. package/dist/crm/testing/source-contract.js +137 -0
  29. package/dist/crm/testing/source.d.ts +33 -0
  30. package/dist/crm/testing/source.js +126 -0
  31. package/dist/crm/testing/sync-contract.d.ts +13 -0
  32. package/dist/crm/testing/sync-contract.js +178 -0
  33. package/dist/domain/crm.d.ts +82 -0
  34. package/dist/domain/crm.js +3 -0
  35. package/dist/entities/deal.d.ts +159 -0
  36. package/dist/entities/deal.js +86 -0
  37. package/dist/entities/index.d.ts +150 -0
  38. package/dist/entities/index.js +5 -0
  39. package/dist/entities/list-results.d.ts +13 -0
  40. package/dist/entities/list-results.js +17 -1
  41. package/dist/errors/reasons.js +5 -0
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.js +1 -0
  44. package/dist/sends/index.d.ts +1 -1
  45. package/dist/sends/index.js +1 -1
  46. package/dist/sends/pace.d.ts +18 -0
  47. package/dist/sends/pace.js +40 -1
  48. package/dist/services/index.d.ts +1 -0
  49. package/dist/services/index.js +1 -0
  50. package/dist/store/index.d.ts +2 -1
  51. package/dist/store/index.js +1 -0
  52. package/dist/store/sqlite/crm.d.ts +48 -0
  53. package/dist/store/sqlite/crm.js +318 -0
  54. package/dist/store/sqlite/deals.d.ts +10 -0
  55. package/dist/store/sqlite/deals.js +33 -15
  56. package/dist/store/sqlite/email-activity.d.ts +3 -0
  57. package/dist/store/sqlite/email-activity.js +11 -0
  58. package/dist/store/store.d.ts +3 -0
  59. package/dist/store/store.js +5 -0
  60. package/package.json +13 -1
  61. package/parity.json +29 -1
@@ -0,0 +1,126 @@
1
+ /** When the fake CRM's changes happened: well before any `now` a contract syncs at. */
2
+ const STAMPED_FROM = Date.UTC(2026, 0, 1);
3
+ export const SALES_PIPELINE = {
4
+ externalId: "deal-1",
5
+ name: "Example sales",
6
+ stages: [
7
+ { externalId: "new", name: "New", position: 1, outcome: "open" },
8
+ { externalId: "quote", name: "Quote sent", position: 2, outcome: "open" },
9
+ { externalId: "won", name: "Won", position: 3, outcome: "won" },
10
+ { externalId: "lost", name: "Lost", position: 4, outcome: "lost" },
11
+ ],
12
+ };
13
+ export const LEAD_PIPELINE = {
14
+ externalId: "lead-1",
15
+ name: "Example leads",
16
+ stages: [
17
+ { externalId: "lead-new", name: "New", position: 1, outcome: "open" },
18
+ { externalId: "lead-good", name: "Converted", position: 2, outcome: "won" },
19
+ { externalId: "lead-junk", name: "Junk", position: 3, outcome: "lost" },
20
+ ],
21
+ };
22
+ export const fakeCrmSource = (options = {}) => {
23
+ const traits = {
24
+ kinds: ["organization", "person", "deal", "lead"],
25
+ removedInChanges: false,
26
+ ...options.traits,
27
+ };
28
+ const size = options.pageSize ?? 2;
29
+ const rows = new Map();
30
+ const pipelines = { deal: [SALES_PIPELINE], lead: [LEAD_PIPELINE] };
31
+ let version = 0;
32
+ const expired = new Map();
33
+ let hidden = false;
34
+ const calls = { pipelines: 0, changes: 0, ids: 0 };
35
+ const of = (kind) => {
36
+ const held = rows.get(kind) ?? new Map();
37
+ rows.set(kind, held);
38
+ return held;
39
+ };
40
+ const window = (list, page) => {
41
+ const at = page === undefined ? 0 : Number(page);
42
+ const next = at + size;
43
+ return { slice: list.slice(at, next), nextPage: next < list.length ? String(next) : undefined };
44
+ };
45
+ return {
46
+ provider: "example-crm",
47
+ traits,
48
+ calls,
49
+ put(kind, record) {
50
+ version += 1;
51
+ // A CRM stamps each change; without one here the store would take its own clock, which an id pass skips.
52
+ const stamped = (kind === "deal" || kind === "lead") && record.updatedAt == null
53
+ ? { ...record, updatedAt: STAMPED_FROM + version * 1000 }
54
+ : record;
55
+ of(kind).set(record.externalId, { record: stamped, version, deleted: false });
56
+ },
57
+ delete(kind, externalId) {
58
+ const held = of(kind).get(externalId);
59
+ if (!held)
60
+ return;
61
+ version += 1;
62
+ of(kind).set(externalId, { ...held, version, deleted: true });
63
+ },
64
+ setPipelines(kind, list) {
65
+ pipelines[kind] = list;
66
+ },
67
+ expireCursors(kind) {
68
+ expired.set(kind, version);
69
+ },
70
+ hideAll(value) {
71
+ hidden = value;
72
+ },
73
+ async pipelines(kind) {
74
+ calls.pipelines += 1;
75
+ return pipelines[kind];
76
+ },
77
+ async changes(kind, cursor, page) {
78
+ calls.changes += 1;
79
+ const since = cursor === null ? null : Number(cursor);
80
+ if (since !== null && since < (expired.get(kind) ?? 0)) {
81
+ expired.delete(kind);
82
+ return { reset: true };
83
+ }
84
+ const changed = [...of(kind).entries()]
85
+ .filter(([, held]) => (since === null ? !held.deleted : held.version > since))
86
+ .filter(([, held]) => !held.deleted || traits.removedInChanges)
87
+ .sort(([, a], [, b]) => a.version - b.version);
88
+ const { slice, nextPage } = window(changed, page);
89
+ return {
90
+ items: slice.filter(([, held]) => !held.deleted).map(([, held]) => held.record),
91
+ ...(traits.removedInChanges ? { removed: slice.filter(([, held]) => held.deleted).map(([id]) => id) } : {}),
92
+ ...(nextPage === undefined ? { cursor: String(version) } : { nextPage }),
93
+ };
94
+ },
95
+ async ids(kind, page) {
96
+ calls.ids += 1;
97
+ const listed = hidden ? [] : [...of(kind).entries()].filter(([, held]) => !held.deleted).map(([id]) => id);
98
+ const { slice, nextPage } = window(listed, page);
99
+ return { ids: slice, ...(nextPage === undefined ? {} : { nextPage }) };
100
+ },
101
+ };
102
+ };
103
+ export const exampleDeal = (externalId, extra = {}) => ({
104
+ externalId,
105
+ title: `Example deal ${externalId}`,
106
+ pipeline: SALES_PIPELINE.externalId,
107
+ stage: "new",
108
+ ...extra,
109
+ });
110
+ export const exampleLead = (externalId, extra = {}) => ({
111
+ externalId,
112
+ title: `Example lead ${externalId}`,
113
+ pipeline: LEAD_PIPELINE.externalId,
114
+ stage: "lead-new",
115
+ ...extra,
116
+ });
117
+ export const examplePerson = (externalId, extra = {}) => ({
118
+ externalId,
119
+ name: `Example person ${externalId}`,
120
+ ...extra,
121
+ });
122
+ export const exampleOrganization = (externalId, extra = {}) => ({
123
+ externalId,
124
+ name: `Example company ${externalId}`,
125
+ ...extra,
126
+ });
@@ -0,0 +1,13 @@
1
+ import type { ContractCase } from "../../meetings/index.js";
2
+ import type { MessageStore } from "../../store/store.js";
3
+ import type { AccountKey } from "../../store/types.js";
4
+ import type { CrmSourceTraits } from "../source.js";
5
+ /** A fresh, empty store and the CRM account to sync into it. */
6
+ export interface CrmSyncFixture {
7
+ store: MessageStore;
8
+ account: AccountKey;
9
+ }
10
+ /** Both ways a CRM may report deletions: the engine must handle each. */
11
+ export declare const CRM_TRAIT_COMBINATIONS: readonly Pick<CrmSourceTraits, "removedInChanges">[];
12
+ /** What the sync engine does over any store, whatever the CRM: core runs it, and a CLI may run it over its store. */
13
+ export declare const crmSyncContract: (make: () => CrmSyncFixture | Promise<CrmSyncFixture>) => ContractCase[];
@@ -0,0 +1,178 @@
1
+ import assert from "node:assert/strict";
2
+ import { runBudget } from "../../background/budget.js";
3
+ import { syncCrm } from "../sync.js";
4
+ import { exampleDeal, exampleLead, exampleOrganization, examplePerson, fakeCrmSource, LEAD_PIPELINE, SALES_PIPELINE, } from "./source.js";
5
+ /** Both ways a CRM may report deletions: the engine must handle each. */
6
+ export const CRM_TRAIT_COMBINATIONS = [
7
+ { removedInChanges: false },
8
+ { removedInChanges: true },
9
+ ];
10
+ const DAY = 86_400_000;
11
+ /** What the sync engine does over any store, whatever the CRM: core runs it, and a CLI may run it over its store. */
12
+ export const crmSyncContract = (make) => CRM_TRAIT_COMBINATIONS.flatMap((traits) => {
13
+ const label = traits.removedInChanges ? "(deletions in changes)" : "(deletions by id pass)";
14
+ let now = Date.now();
15
+ const test = (name, check) => ({
16
+ name: `${name} ${label}`,
17
+ async run() {
18
+ now = Date.now();
19
+ const fixture = await make();
20
+ const source = fakeCrmSource({ traits });
21
+ const sync = (options = {}) => syncCrm(source, fixture.store, fixture.account, { now, ...options });
22
+ const deals = async () => (await fixture.store.deals.list({ account: fixture.account, kind: "deal" }))
23
+ .map(({ externalId }) => externalId)
24
+ .sort();
25
+ await check({ ...fixture, source, sync, deals });
26
+ },
27
+ });
28
+ const seeded = (source, count = 3) => {
29
+ for (let at = 1; at <= count; at += 1)
30
+ source.put("deal", exampleDeal(`d${at}`));
31
+ };
32
+ return [
33
+ test("copies every kind on the first run, and only what changed on the next", async ({ source, sync, deals, store, account, }) => {
34
+ source.put("organization", exampleOrganization("c1"));
35
+ source.put("person", examplePerson("p1"));
36
+ seeded(source);
37
+ source.put("lead", exampleLead("l1"));
38
+ const first = await sync();
39
+ assert.equal(first.state, "complete");
40
+ assert.deepEqual(await deals(), ["d1", "d2", "d3"]);
41
+ assert.equal((await store.deals.list({ account, kind: "lead" })).length, 1);
42
+ assert.ok(first.synced.every(({ round }) => round === "full"));
43
+ source.put("deal", exampleDeal("d2", { stage: "won", title: "Example deal won" }));
44
+ const second = await sync();
45
+ assert.deepEqual(second.synced.map(({ kind, round, saved }) => [kind, round, saved]), [
46
+ ["organization", "changes", 0],
47
+ ["person", "changes", 0],
48
+ ["deal", "changes", 1],
49
+ ["lead", "changes", 0],
50
+ ]);
51
+ const [won] = await store.deals.list({ account, status: "won" });
52
+ assert.equal(won?.title, "Example deal won");
53
+ }),
54
+ test("marks deleted what the CRM deleted, after a complete list at the latest", async ({ source, sync, deals, }) => {
55
+ seeded(source);
56
+ await sync();
57
+ source.delete("deal", "d2");
58
+ now += DAY;
59
+ const run = await sync();
60
+ assert.deepEqual(await deals(), ["d1", "d3"]);
61
+ const deal = run.synced.find(({ kind }) => kind === "deal");
62
+ assert.equal((deal?.removed ?? 0) + (deal?.unlisted ?? 0), 1);
63
+ }),
64
+ test("a list of ids the budget stops marks nothing, and the next run goes on from it", async ({ source, sync, deals, }) => {
65
+ if (traits.removedInChanges)
66
+ return;
67
+ seeded(source, 5);
68
+ await sync();
69
+ source.delete("deal", "d1");
70
+ now += DAY;
71
+ const stopped = await sync({ budget: runBudget({ requests: 7 }) });
72
+ assert.equal(stopped.state, "partial");
73
+ assert.equal(stopped.hint, "run sync again to continue");
74
+ assert.deepEqual(await deals(), ["d1", "d2", "d3", "d4", "d5"]);
75
+ const finished = await sync();
76
+ assert.equal(finished.state, "complete");
77
+ assert.deepEqual(await deals(), ["d2", "d3", "d4", "d5"]);
78
+ }),
79
+ test("a list that would mark most rows deleted is held as an issue until --rescan", async ({ source, sync, deals, }) => {
80
+ if (traits.removedInChanges)
81
+ return;
82
+ seeded(source, 12);
83
+ await sync();
84
+ source.hideAll(true);
85
+ now += DAY;
86
+ const held = await sync();
87
+ assert.equal(held.synced.find(({ kind }) => kind === "deal")?.pass, "held");
88
+ assert.ok(held.issues.some(({ reason }) => reason === "crm.unlisted-held"));
89
+ assert.equal((await deals()).length, 12);
90
+ await sync({ rescan: true });
91
+ assert.deepEqual(await deals(), []);
92
+ }),
93
+ test("a dead cursor starts a full round, marked reset", async ({ source, sync, deals }) => {
94
+ seeded(source);
95
+ await sync();
96
+ source.put("deal", exampleDeal("d4"));
97
+ source.expireCursors("deal");
98
+ const run = await sync();
99
+ const deal = run.synced.find(({ kind }) => kind === "deal");
100
+ assert.equal(deal?.reset, true);
101
+ assert.equal(deal?.round, "full");
102
+ assert.deepEqual(await deals(), ["d1", "d2", "d3", "d4"]);
103
+ }),
104
+ test("a run its budget stops saves its pages and the next goes on from the page it stopped at", async ({ source, sync, deals, }) => {
105
+ seeded(source, 9);
106
+ const stopped = await sync({ budget: runBudget({ requests: 6 }) });
107
+ assert.equal(stopped.state, "partial");
108
+ assert.equal(stopped.stopped, "requests");
109
+ const kept = (await deals()).length;
110
+ assert.ok(kept > 0 && kept < 9, `some pages kept, not all: ${kept}`);
111
+ const finished = await sync();
112
+ assert.equal(finished.state, "complete");
113
+ assert.equal((await deals()).length, 9);
114
+ assert.equal(finished.synced.find(({ kind }) => kind === "deal")?.saved, 9 - kept, "it went on, not from the start");
115
+ }),
116
+ test("an item in a stage made after the pipelines were read is kept once they are read again", async ({ source, sync, deals, }) => {
117
+ const changes = source.changes.bind(source);
118
+ source.changes = async (kind, cursor, page) => {
119
+ if (kind === "deal") {
120
+ source.setPipelines("deal", [
121
+ {
122
+ ...SALES_PIPELINE,
123
+ stages: [...SALES_PIPELINE.stages, { externalId: "late", name: "Late", position: 5, outcome: "open" }],
124
+ },
125
+ ]);
126
+ source.put("deal", exampleDeal("d1", { stage: "late" }));
127
+ }
128
+ return changes(kind, cursor, page);
129
+ };
130
+ const run = await sync();
131
+ assert.deepEqual(await deals(), ["d1"]);
132
+ assert.deepEqual(run.issues, []);
133
+ }),
134
+ test("an item in a stage the CRM never lists is named as an issue, and the round goes on", async ({ source, sync, deals, }) => {
135
+ source.put("deal", exampleDeal("d1", { stage: "nowhere" }));
136
+ source.put("deal", exampleDeal("d2"));
137
+ const run = await sync();
138
+ assert.deepEqual(await deals(), ["d2"]);
139
+ assert.deepEqual(run.issues.map(({ kind, reason, externalId }) => [kind, reason, externalId]), [["deal", "crm.stage-unknown", "d1"]]);
140
+ }),
141
+ test("a pipeline sharing the other kind's pipeline id is named as an issue, and the rest syncs", async ({ source, sync, deals, }) => {
142
+ source.setPipelines("lead", [{ ...LEAD_PIPELINE, externalId: SALES_PIPELINE.externalId }]);
143
+ source.put("deal", exampleDeal("d1"));
144
+ const run = await sync();
145
+ assert.deepEqual(await deals(), ["d1"]);
146
+ assert.ok(run.issues.some(({ kind, reason }) => kind === "lead" && reason === "crm.pipeline-clash"));
147
+ }),
148
+ test("a contact joins a person by exact email, and a company never joins an organization by name", async ({ source, sync, store, account, }) => {
149
+ const own = await store.knowledge.addOrganization({ kind: "company", name: "Example company c1" });
150
+ source.put("organization", exampleOrganization("c1"));
151
+ source.put("person", examplePerson("p1", { email: "person@example.com" }));
152
+ source.put("deal", exampleDeal("d1", { organization: "c1", people: [{ externalId: "p1", role: "contact" }] }));
153
+ await sync();
154
+ const [deal] = await store.deals.list({ account });
155
+ assert.notEqual(deal?.organization?.ref, own.ref);
156
+ assert.equal(deal?.organization?.name, "Example company c1");
157
+ const person = await store.people.personOf({ provider: "email", id: "person@example.com" });
158
+ assert.equal(deal?.people[0]?.ref, `person:${person?.uid}`);
159
+ }),
160
+ test("a cancelled run stops and keeps what it saved", async ({ source, sync, deals }) => {
161
+ seeded(source, 4);
162
+ const controller = new AbortController();
163
+ const pipelines = source.pipelines.bind(source);
164
+ source.pipelines = async (kind) => {
165
+ if (kind === "lead")
166
+ controller.abort();
167
+ return pipelines(kind);
168
+ };
169
+ const run = await sync({ signal: controller.signal });
170
+ assert.equal(run.stopped, "cancelled");
171
+ assert.equal(run.state, "partial");
172
+ assert.deepEqual(await deals(), []);
173
+ const again = await sync();
174
+ assert.equal(again.state, "complete");
175
+ assert.equal((await deals()).length, 4);
176
+ }),
177
+ ];
178
+ });
@@ -0,0 +1,82 @@
1
+ import type { DealKind, DealRole, DealStatus, StageOutcome } from "./deals.js";
2
+ /** What a CRM holds that the store copies: its deals and leads, and the contacts and companies on them. */
3
+ export declare const CRM_KINDS: readonly ["organization", "person", "deal", "lead"];
4
+ export type CrmKind = (typeof CRM_KINDS)[number];
5
+ /** One stage of a CRM pipeline, as the CRM orders and marks it. */
6
+ export interface CrmStage {
7
+ /** Unique in its pipeline. */
8
+ externalId: string;
9
+ name: string;
10
+ /** Any numbers; the store keeps the order they give. */
11
+ position: number;
12
+ outcome: StageOutcome;
13
+ }
14
+ export interface CrmPipeline {
15
+ /**
16
+ * Unique among the account's pipelines of its kind. Until the store's key is widened, one with the id of a stored
17
+ * pipeline of the other kind is skipped and named.
18
+ */
19
+ externalId: string;
20
+ name: string;
21
+ stages: CrmStage[];
22
+ }
23
+ /** A deal or a lead as the CRM has it. Every reference is the CRM's own id; the store resolves them. */
24
+ export interface CrmItem {
25
+ externalId: string;
26
+ title: string;
27
+ pipeline: string;
28
+ stage: string;
29
+ /**
30
+ * Whether it is still open, won or lost, where the CRM keeps that on the item (Pipedrive's `status`); left out, the
31
+ * stage's outcome says it.
32
+ */
33
+ status?: DealStatus | null;
34
+ /** A decimal string in `currency`; both or neither. */
35
+ amount?: string | null;
36
+ /** ISO 4217. */
37
+ currency?: string | null;
38
+ /** `YYYY-MM-DD`: when it is expected to close. */
39
+ closeDate?: string | null;
40
+ /** `YYYY-MM-DD`: when it was won or lost. */
41
+ closedDate?: string | null;
42
+ /** A company's id. */
43
+ organization?: string | null;
44
+ /** Contacts on it, by their id, and the part each plays. */
45
+ people?: {
46
+ externalId: string;
47
+ role: DealRole;
48
+ }[];
49
+ /** A lead's: the deal it became. */
50
+ deal?: string | null;
51
+ /** Epoch milliseconds, as the CRM gives them; the stage history counts from these. */
52
+ createdAt?: number | null;
53
+ updatedAt?: number | null;
54
+ /** The CRM's own page for it, http or https. */
55
+ url?: string | null;
56
+ /** What the CRM sends that has no column, such as custom fields with their labels; never a secret. */
57
+ metadata?: Record<string, unknown>;
58
+ }
59
+ /**
60
+ * A CRM contact: it becomes an identity of the CRM's provider, `<account>/contact/<id>`, joined to a person by exact
61
+ * email only. A CRM user, such as a deal's owner, is not a contact: its ids are another sequence (`<account>/user/<id>`).
62
+ */
63
+ export interface CrmPerson {
64
+ externalId: string;
65
+ name: string | null;
66
+ email?: string | null;
67
+ phone?: string | null;
68
+ }
69
+ /** A CRM company: it becomes an organization of its own, never one matched by name. */
70
+ export interface CrmOrganization {
71
+ externalId: string;
72
+ name: string;
73
+ url?: string | null;
74
+ metadata?: Record<string, unknown>;
75
+ }
76
+ export interface CrmRecords {
77
+ deal: CrmItem;
78
+ lead: CrmItem;
79
+ person: CrmPerson;
80
+ organization: CrmOrganization;
81
+ }
82
+ export declare const isItemKind: (kind: CrmKind) => kind is DealKind;
@@ -0,0 +1,3 @@
1
+ /** What a CRM holds that the store copies: its deals and leads, and the contacts and companies on them. */
2
+ export const CRM_KINDS = ["organization", "person", "deal", "lead"];
3
+ export const isItemKind = (kind) => kind === "deal" || kind === "lead";
@@ -0,0 +1,159 @@
1
+ import * as v from "valibot";
2
+ /**
3
+ * A deal (`deal:<id>`), the owner's own or a CRM's copy: one shape for memo and every CRM CLI. Money is a decimal
4
+ * string in its currency, both `null` when no amount is expected.
5
+ */
6
+ export declare const deal: v.StrictObjectSchema<{
7
+ readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
8
+ readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
9
+ readonly kind: v.PicklistSchema<["deal", "lead"], undefined>;
10
+ readonly title: v.StringSchema<undefined>;
11
+ /** One line with the stage, amount and status. */
12
+ readonly summary: v.StringSchema<undefined>;
13
+ readonly pipeline: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
14
+ readonly pipeline_name: v.StringSchema<undefined>;
15
+ readonly stage: v.StringSchema<undefined>;
16
+ readonly status: v.PicklistSchema<["open", "won", "lost"], undefined>;
17
+ readonly organization: v.NullableSchema<v.StrictObjectSchema<{
18
+ readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
19
+ readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
20
+ readonly owner: v.BooleanSchema<undefined>;
21
+ }, undefined>, undefined>;
22
+ readonly currency: v.NullableSchema<v.StringSchema<undefined>, undefined>;
23
+ readonly amount: v.NullableSchema<v.StringSchema<undefined>, undefined>;
24
+ readonly close_date: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
25
+ readonly closed_date: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
26
+ readonly closed_reason: v.NullableSchema<v.StringSchema<undefined>, undefined>;
27
+ readonly rank: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
28
+ /** A lead's deal, once it became one. */
29
+ readonly deal: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
30
+ /** The CRM account it is a copy from; `null` for the owner's own. */
31
+ readonly account: v.NullableSchema<v.StrictObjectSchema<{
32
+ readonly provider: v.StringSchema<undefined>;
33
+ readonly account: v.StringSchema<undefined>;
34
+ }, undefined>, undefined>;
35
+ readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
36
+ readonly people: v.ArraySchema<v.StrictObjectSchema<{
37
+ readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
38
+ readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
39
+ readonly owner: v.BooleanSchema<undefined>;
40
+ readonly role: v.PicklistSchema<["owner", "contact", "decision-maker"], undefined>;
41
+ }, undefined>, undefined>;
42
+ readonly author: v.StrictObjectSchema<{
43
+ readonly type: v.PicklistSchema<["person", "bot"], undefined>;
44
+ readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
45
+ readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
46
+ readonly kind: v.NullableSchema<v.PicklistSchema<["agent", "rule"], undefined>, undefined>;
47
+ }, undefined>;
48
+ readonly model: v.NullableSchema<v.StringSchema<undefined>, undefined>;
49
+ readonly source: v.StringSchema<undefined>;
50
+ readonly reason: v.NullableSchema<v.StringSchema<undefined>, undefined>;
51
+ readonly evidence: v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
52
+ readonly project: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
53
+ readonly project_name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
54
+ readonly created_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
55
+ readonly updated_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
56
+ readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
57
+ }, undefined>;
58
+ /** A lead (`lead:<id>`): a deal's shape, in a lead pipeline. */
59
+ export declare const lead: v.StrictObjectSchema<{
60
+ readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
61
+ readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
62
+ readonly kind: v.PicklistSchema<["deal", "lead"], undefined>;
63
+ readonly title: v.StringSchema<undefined>;
64
+ /** One line with the stage, amount and status. */
65
+ readonly summary: v.StringSchema<undefined>;
66
+ readonly pipeline: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
67
+ readonly pipeline_name: v.StringSchema<undefined>;
68
+ readonly stage: v.StringSchema<undefined>;
69
+ readonly status: v.PicklistSchema<["open", "won", "lost"], undefined>;
70
+ readonly organization: v.NullableSchema<v.StrictObjectSchema<{
71
+ readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
72
+ readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
73
+ readonly owner: v.BooleanSchema<undefined>;
74
+ }, undefined>, undefined>;
75
+ readonly currency: v.NullableSchema<v.StringSchema<undefined>, undefined>;
76
+ readonly amount: v.NullableSchema<v.StringSchema<undefined>, undefined>;
77
+ readonly close_date: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
78
+ readonly closed_date: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
79
+ readonly closed_reason: v.NullableSchema<v.StringSchema<undefined>, undefined>;
80
+ readonly rank: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
81
+ /** A lead's deal, once it became one. */
82
+ readonly deal: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
83
+ /** The CRM account it is a copy from; `null` for the owner's own. */
84
+ readonly account: v.NullableSchema<v.StrictObjectSchema<{
85
+ readonly provider: v.StringSchema<undefined>;
86
+ readonly account: v.StringSchema<undefined>;
87
+ }, undefined>, undefined>;
88
+ readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
89
+ readonly people: v.ArraySchema<v.StrictObjectSchema<{
90
+ readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
91
+ readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
92
+ readonly owner: v.BooleanSchema<undefined>;
93
+ readonly role: v.PicklistSchema<["owner", "contact", "decision-maker"], undefined>;
94
+ }, undefined>, undefined>;
95
+ readonly author: v.StrictObjectSchema<{
96
+ readonly type: v.PicklistSchema<["person", "bot"], undefined>;
97
+ readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
98
+ readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
99
+ readonly kind: v.NullableSchema<v.PicklistSchema<["agent", "rule"], undefined>, undefined>;
100
+ }, undefined>;
101
+ readonly model: v.NullableSchema<v.StringSchema<undefined>, undefined>;
102
+ readonly source: v.StringSchema<undefined>;
103
+ readonly reason: v.NullableSchema<v.StringSchema<undefined>, undefined>;
104
+ readonly evidence: v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
105
+ readonly project: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
106
+ readonly project_name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
107
+ readonly created_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
108
+ readonly updated_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
109
+ readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
110
+ }, undefined>;
111
+ /** A pipeline (`pipeline:<id>`) with its stages in order; `pipelines show` adds the funnel. */
112
+ export declare const pipeline: v.StrictObjectSchema<{
113
+ readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
114
+ readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
115
+ readonly name: v.StringSchema<undefined>;
116
+ readonly kind: v.PicklistSchema<["deal", "lead"], undefined>;
117
+ readonly account: v.NullableSchema<v.StrictObjectSchema<{
118
+ readonly provider: v.StringSchema<undefined>;
119
+ readonly account: v.StringSchema<undefined>;
120
+ }, undefined>, undefined>;
121
+ readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
122
+ readonly stages: v.ArraySchema<v.StrictObjectSchema<{
123
+ readonly name: v.StringSchema<undefined>;
124
+ readonly position: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
125
+ readonly outcome: v.PicklistSchema<["open", "won", "lost"], undefined>;
126
+ readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
127
+ /** Open deals or leads in it now. */
128
+ readonly open: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
129
+ /** With a funnel: rows that got this far, and the share of them that reached the next stage. */
130
+ readonly reached: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
131
+ readonly conversion: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
132
+ }, undefined>, undefined>;
133
+ readonly created_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
134
+ readonly totals: v.OptionalSchema<v.StrictObjectSchema<{
135
+ readonly items: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
136
+ readonly open: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
137
+ readonly won: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
138
+ readonly lost: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
139
+ readonly win_rate: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
140
+ }, undefined>, undefined>;
141
+ readonly since: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>, undefined>;
142
+ readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
143
+ }, undefined>;
144
+ /** One stage of a pipeline, with the pipeline it is in: what `stages list` answers. */
145
+ export declare const stage: v.StrictObjectSchema<{
146
+ readonly name: v.StringSchema<undefined>;
147
+ readonly position: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
148
+ readonly outcome: v.PicklistSchema<["open", "won", "lost"], undefined>;
149
+ readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
150
+ /** Open deals or leads in it now. */
151
+ readonly open: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
152
+ /** With a funnel: rows that got this far, and the share of them that reached the next stage. */
153
+ readonly reached: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
154
+ readonly conversion: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
155
+ readonly pipeline: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
156
+ readonly pipeline_name: v.StringSchema<undefined>;
157
+ readonly kind: v.PicklistSchema<["deal", "lead"], undefined>;
158
+ readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
159
+ }, undefined>;
@@ -0,0 +1,86 @@
1
+ import * as v from "valibot";
2
+ import { count, id, reference, text, time, untrusted } from "./fields.js";
3
+ const nullableText = v.nullable(text);
4
+ const party = v.strictObject({ ref: reference, name: nullableText, owner: v.boolean() });
5
+ const outcome = v.picklist(["open", "won", "lost"]);
6
+ const crmAccount = v.nullable(v.strictObject({ provider: text, account: text }));
7
+ const day = v.nullable(v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}$/)));
8
+ /**
9
+ * A deal (`deal:<id>`), the owner's own or a CRM's copy: one shape for memo and every CRM CLI. Money is a decimal
10
+ * string in its currency, both `null` when no amount is expected.
11
+ */
12
+ export const deal = v.strictObject({
13
+ id,
14
+ ref: reference,
15
+ kind: v.picklist(["deal", "lead"]),
16
+ title: text,
17
+ /** One line with the stage, amount and status. */
18
+ summary: text,
19
+ pipeline: reference,
20
+ pipeline_name: text,
21
+ stage: text,
22
+ status: outcome,
23
+ organization: v.nullable(party),
24
+ currency: nullableText,
25
+ amount: nullableText,
26
+ close_date: day,
27
+ closed_date: day,
28
+ closed_reason: nullableText,
29
+ rank: v.nullable(v.number()),
30
+ /** A lead's deal, once it became one. */
31
+ deal: v.nullable(reference),
32
+ /** The CRM account it is a copy from; `null` for the owner's own. */
33
+ account: crmAccount,
34
+ external_id: nullableText,
35
+ people: v.array(v.strictObject({ ...party.entries, role: v.picklist(["owner", "contact", "decision-maker"]) })),
36
+ author: v.strictObject({
37
+ type: v.picklist(["person", "bot"]),
38
+ id,
39
+ name: nullableText,
40
+ kind: v.nullable(v.picklist(["agent", "rule"])),
41
+ }),
42
+ model: nullableText,
43
+ source: text,
44
+ reason: nullableText,
45
+ evidence: v.array(reference),
46
+ project: v.nullable(reference),
47
+ project_name: nullableText,
48
+ created_at: time,
49
+ updated_at: time,
50
+ untrusted,
51
+ });
52
+ /** A lead (`lead:<id>`): a deal's shape, in a lead pipeline. */
53
+ export const lead = deal;
54
+ const pipelineStage = v.strictObject({
55
+ name: text,
56
+ position: count,
57
+ outcome,
58
+ external_id: nullableText,
59
+ /** Open deals or leads in it now. */
60
+ open: count,
61
+ /** With a funnel: rows that got this far, and the share of them that reached the next stage. */
62
+ reached: v.optional(count),
63
+ conversion: v.optional(v.nullable(v.number())),
64
+ });
65
+ /** A pipeline (`pipeline:<id>`) with its stages in order; `pipelines show` adds the funnel. */
66
+ export const pipeline = v.strictObject({
67
+ id,
68
+ ref: reference,
69
+ name: text,
70
+ kind: v.picklist(["deal", "lead"]),
71
+ account: crmAccount,
72
+ external_id: nullableText,
73
+ stages: v.array(pipelineStage),
74
+ created_at: time,
75
+ totals: v.optional(v.strictObject({ items: count, open: count, won: count, lost: count, win_rate: v.nullable(v.number()) })),
76
+ since: v.optional(day),
77
+ untrusted,
78
+ });
79
+ /** One stage of a pipeline, with the pipeline it is in: what `stages list` answers. */
80
+ export const stage = v.strictObject({
81
+ ...pipelineStage.entries,
82
+ pipeline: reference,
83
+ pipeline_name: text,
84
+ kind: v.picklist(["deal", "lead"]),
85
+ untrusted,
86
+ });