@wirecat/cli-core 0.593.0 → 0.595.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 (52) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/background/schedules.d.ts +10 -1
  3. package/dist/background/schedules.js +42 -5
  4. package/dist/cli/index.d.ts +1 -1
  5. package/dist/cli/messenger/store-maintenance-command.js +54 -5
  6. package/dist/cli/schedule/schedule-command.d.ts +4 -2
  7. package/dist/cli/schedule/schedule-command.js +3 -1
  8. package/dist/crm/cli/index.d.ts +6 -0
  9. package/dist/crm/cli/index.js +118 -2
  10. package/dist/crm/index.d.ts +2 -0
  11. package/dist/crm/index.js +2 -0
  12. package/dist/crm/testing/index.d.ts +2 -1
  13. package/dist/crm/testing/index.js +2 -1
  14. package/dist/crm/testing/source-contract.js +2 -2
  15. package/dist/crm/testing/source.d.ts +22 -2
  16. package/dist/crm/testing/source.js +117 -1
  17. package/dist/crm/testing/sync-contract.js +3 -2
  18. package/dist/crm/testing/write-contract.d.ts +21 -0
  19. package/dist/crm/testing/write-contract.js +127 -0
  20. package/dist/crm/writer.d.ts +76 -0
  21. package/dist/crm/writer.js +14 -0
  22. package/dist/crm/writes.d.ts +68 -0
  23. package/dist/crm/writes.js +245 -0
  24. package/dist/domain/crm.d.ts +3 -4
  25. package/dist/entities/index.d.ts +14 -2
  26. package/dist/entities/store-maintenance.d.ts +14 -2
  27. package/dist/entities/store-maintenance.js +19 -6
  28. package/dist/errors/reasons.js +2 -1
  29. package/dist/mail/cli/review.js +11 -4
  30. package/dist/mail/marks.d.ts +2 -0
  31. package/dist/mail/marks.js +8 -6
  32. package/dist/mail/newsletter.d.ts +10 -0
  33. package/dist/mail/newsletter.js +70 -0
  34. package/dist/mail/review.d.ts +1 -0
  35. package/dist/store/migrations.d.ts +11 -2
  36. package/dist/store/migrations.js +32 -2
  37. package/dist/store/sqlite/crm.d.ts +3 -1
  38. package/dist/store/sqlite/crm.js +37 -20
  39. package/dist/store/sqlite/deals.js +6 -4
  40. package/dist/store/sqlite/email-activity.d.ts +4 -0
  41. package/dist/store/sqlite/email-activity.js +5 -1
  42. package/dist/store/sqlite/manifest.d.ts +2 -0
  43. package/dist/store/sqlite/manifest.js +58 -5
  44. package/dist/store/sqlite/migrations.generated.js +9 -0
  45. package/dist/store/sqlite/open-store.js +8 -1
  46. package/dist/store/sqlite/rollback-copy.d.ts +49 -0
  47. package/dist/store/sqlite/rollback-copy.js +290 -0
  48. package/dist/store/sqlite/schema.d.ts +32 -0
  49. package/dist/store/sqlite/schema.js +4 -2
  50. package/dist/store/store.d.ts +1 -0
  51. package/dist/store/store.js +1 -0
  52. package/package.json +2 -2
@@ -1,3 +1,7 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { CRM_VERBS } from "../writer.js";
3
+ /** Custom codes the fake refuses: each is a neutral field under another name. */
4
+ export const FAKE_CRM_ALIASES = ["stage", "pipeline", "owner", "status"];
1
5
  /** When the fake CRM's changes happened: well before any `now` a contract syncs at. */
2
6
  const STAMPED_FROM = Date.UTC(2026, 0, 1);
3
7
  export const SALES_PIPELINE = {
@@ -32,6 +36,16 @@ export const fakeCrmSource = (options = {}) => {
32
36
  const expired = new Map();
33
37
  let hidden = false;
34
38
  const calls = { pipelines: 0, changes: 0, ids: 0 };
39
+ const capabilities = {
40
+ kinds: ["deal", "lead"],
41
+ verbs: CRM_VERBS,
42
+ idempotent: false,
43
+ ...options.capabilities,
44
+ };
45
+ const writes = [];
46
+ const answered = new Map();
47
+ let made = 0;
48
+ let lose = false;
35
49
  const of = (kind) => {
36
50
  const held = rows.get(kind) ?? new Map();
37
51
  rows.set(kind, held);
@@ -42,10 +56,111 @@ export const fakeCrmSource = (options = {}) => {
42
56
  const next = at + size;
43
57
  return { slice: list.slice(at, next), nextPage: next < list.length ? String(next) : undefined };
44
58
  };
45
- return {
59
+ const held = (kind, externalId) => {
60
+ const found = of(kind).get(externalId);
61
+ if (!found || found.deleted)
62
+ throw new CliError("not_found", `the CRM has no ${kind} ${externalId}`);
63
+ return found.record;
64
+ };
65
+ const placed = (kind, pipelineId, stageId) => {
66
+ const pipeline = pipelineId === undefined ? pipelines[kind][0] : pipelines[kind].find((one) => one.externalId === pipelineId);
67
+ const stage = stageId === undefined ? pipeline?.stages[0] : pipeline?.stages.find((one) => one.externalId === stageId);
68
+ if (!pipeline || !stage)
69
+ throw new CliError("validation_error", `the CRM has no stage ${stageId} there`);
70
+ return { pipeline: pipeline.externalId, stage: stage.externalId, outcome: stage.outcome };
71
+ };
72
+ const applied = (item, fields) => {
73
+ const { custom, owner, pipeline, stage, ...named } = fields;
74
+ const metadata = {
75
+ ...item.metadata,
76
+ ...(owner === undefined ? {} : { owner }),
77
+ ...(custom ? { custom: { ...item.metadata?.custom, ...custom } } : {}),
78
+ };
79
+ return {
80
+ ...item,
81
+ ...Object.fromEntries(Object.entries(named).filter(([, value]) => value !== undefined)),
82
+ ...(pipeline === undefined ? {} : { pipeline }),
83
+ ...(stage === undefined ? {} : { stage }),
84
+ metadata,
85
+ updatedAt: null,
86
+ };
87
+ };
88
+ const refuseAliases = (fields) => {
89
+ const refused = Object.keys(fields.custom ?? {}).find((code) => FAKE_CRM_ALIASES.includes(code));
90
+ if (refused)
91
+ throw new CliError("validation_error", `${refused} is not a custom field`);
92
+ };
93
+ /** Each write once: a repeated idempotent id answers its first outcome; a lost answer still made the change. */
94
+ const write = (verb, kind, externalId, operationId, act) => {
95
+ writes.push({ verb, kind, externalId, operationId });
96
+ if (capabilities.idempotent && answered.has(operationId))
97
+ return answered.get(operationId);
98
+ const done = act();
99
+ if (capabilities.idempotent)
100
+ answered.set(operationId, done);
101
+ if (lose) {
102
+ lose = false;
103
+ throw new CliError("outcome_unknown", "the CRM did not answer");
104
+ }
105
+ return done;
106
+ };
107
+ const saved = (kind, item) => {
108
+ fake.put(kind, item);
109
+ return held(kind, item.externalId);
110
+ };
111
+ const fake = {
46
112
  provider: "example-crm",
47
113
  traits,
48
114
  calls,
115
+ capabilities,
116
+ writes,
117
+ loseNextAnswer() {
118
+ lose = true;
119
+ },
120
+ async create(kind, draft, { operationId }) {
121
+ refuseAliases(draft);
122
+ return write("create", kind, null, operationId, () => {
123
+ const { pipeline, stage } = placed(kind, draft.pipeline, draft.stage);
124
+ made += 1;
125
+ const blank = { externalId: `${kind}-new-${made}`, title: draft.title, pipeline, stage };
126
+ return saved(kind, applied(blank, { ...draft, pipeline, stage }));
127
+ });
128
+ },
129
+ async update(kind, externalId, patch, { operationId }) {
130
+ refuseAliases(patch);
131
+ return write("update", kind, externalId, operationId, () => {
132
+ const item = held(kind, externalId);
133
+ const at = patch.stage === undefined ? {} : placed(kind, patch.pipeline ?? item.pipeline, patch.stage);
134
+ return saved(kind, applied(item, { ...patch, ...at }));
135
+ });
136
+ },
137
+ async move(kind, externalId, stage, { operationId }) {
138
+ return write("move", kind, externalId, operationId, () => {
139
+ const item = held(kind, externalId);
140
+ return saved(kind, { ...item, stage: placed(kind, item.pipeline, stage).stage, status: null, updatedAt: null });
141
+ });
142
+ },
143
+ async close(kind, externalId, outcome, { operationId, reason }) {
144
+ return write("close", kind, externalId, operationId, () => {
145
+ const item = held(kind, externalId);
146
+ const pipeline = pipelines[kind].find((one) => one.externalId === item.pipeline);
147
+ const closing = pipeline?.stages.find((one) => one.outcome === outcome);
148
+ return saved(kind, {
149
+ ...item,
150
+ ...(closing ? { stage: closing.externalId } : {}),
151
+ status: outcome,
152
+ closedReason: reason ?? null,
153
+ updatedAt: null,
154
+ });
155
+ });
156
+ },
157
+ async remove(kind, externalId, { operationId }) {
158
+ write("remove", kind, externalId, operationId, () => {
159
+ held(kind, externalId);
160
+ fake.delete(kind, externalId);
161
+ return undefined;
162
+ });
163
+ },
49
164
  put(kind, record) {
50
165
  version += 1;
51
166
  // A CRM stamps each change; without one here the store would take its own clock, which an id pass skips.
@@ -99,6 +214,7 @@ export const fakeCrmSource = (options = {}) => {
99
214
  return { ids: slice, ...(nextPage === undefined ? {} : { nextPage }) };
100
215
  },
101
216
  };
217
+ return fake;
102
218
  };
103
219
  export const exampleDeal = (externalId, extra = {}) => ({
104
220
  externalId,
@@ -138,12 +138,13 @@ export const crmSyncContract = (make) => CRM_TRAIT_COMBINATIONS.flatMap((traits)
138
138
  assert.deepEqual(await deals(), ["d2"]);
139
139
  assert.deepEqual(run.issues.map(({ kind, reason, externalId }) => [kind, reason, externalId]), [["deal", "crm.stage-unknown", "d1"]]);
140
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, }) => {
141
+ test("a lead pipeline may share a deal pipeline's id", async ({ source, sync, deals }) => {
142
142
  source.setPipelines("lead", [{ ...LEAD_PIPELINE, externalId: SALES_PIPELINE.externalId }]);
143
143
  source.put("deal", exampleDeal("d1"));
144
+ source.put("lead", exampleLead("l1", { pipeline: SALES_PIPELINE.externalId }));
144
145
  const run = await sync();
145
146
  assert.deepEqual(await deals(), ["d1"]);
146
- assert.ok(run.issues.some(({ kind, reason }) => kind === "lead" && reason === "crm.pipeline-clash"));
147
+ assert.deepEqual(run.issues, []);
147
148
  }),
148
149
  test("a contact joins a person by exact email, and a company never joins an organization by name", async ({ source, sync, store, account, }) => {
149
150
  const own = await store.knowledge.addOrganization({ kind: "company", name: "Example company c1" });
@@ -0,0 +1,21 @@
1
+ import type { CrmPipeline } from "../../domain/crm.js";
2
+ import { type DealKind } from "../../domain/deals.js";
3
+ import type { ContractCase } from "../../meetings/index.js";
4
+ import { type CrmWriter } from "../writer.js";
5
+ /**
6
+ * One adapter over recorded or faked responses, fresh for each case, that takes writes of `kind`. `pipeline` is one
7
+ * of the kind's pipelines with at least two open stages and a won one.
8
+ */
9
+ export interface CrmWriteFixture {
10
+ writer: CrmWriter;
11
+ kind?: DealKind;
12
+ pipeline: CrmPipeline;
13
+ /** Custom field codes that are a neutral field under the CRM's own name (Bitrix24's `stageId`, `ASSIGNED_BY_ID`). */
14
+ aliases: readonly string[];
15
+ /** Makes the next write's answer lost after the CRM got it, as a dropped connection does. */
16
+ loseNextAnswer: () => void;
17
+ /** How many write requests reached the CRM so far. */
18
+ requests: () => number;
19
+ }
20
+ /** What every CRM adapter's write side does, whatever the CRM: run each case in the adapter's tests. */
21
+ export declare const crmWriteContract: (make: () => CrmWriteFixture | Promise<CrmWriteFixture>) => ContractCase[];
@@ -0,0 +1,127 @@
1
+ import assert from "node:assert/strict";
2
+ import { isCliError } from "@wirecat/cli-base";
3
+ import { DEAL_KINDS } from "../../domain/deals.js";
4
+ import { CRM_VERBS } from "../writer.js";
5
+ let operations = 0;
6
+ const operation = () => {
7
+ operations += 1;
8
+ return { operationId: `contract-${Date.now()}-${operations}` };
9
+ };
10
+ const refusedWith = async (code, act) => {
11
+ try {
12
+ await act();
13
+ }
14
+ catch (error) {
15
+ assert.ok(isCliError(error), `a CliError, not ${String(error)}`);
16
+ assert.equal(error.code, code);
17
+ return;
18
+ }
19
+ assert.fail(`expected ${code}`);
20
+ };
21
+ const outcomeOf = (pipeline, item) => item.status ?? pipeline.stages.find((stage) => stage.externalId === item.stage)?.outcome;
22
+ /** What every CRM adapter's write side does, whatever the CRM: run each case in the adapter's tests. */
23
+ export const crmWriteContract = (make) => {
24
+ const openStages = async () => {
25
+ const fixture = await make();
26
+ const open = fixture.pipeline.stages.filter((stage) => stage.outcome === "open");
27
+ assert.ok(open.length >= 2, "the fixture's pipeline has two open stages");
28
+ const kind = fixture.kind ?? "deal";
29
+ const made = await fixture.writer.create(kind, {
30
+ title: "Example contract deal",
31
+ pipeline: fixture.pipeline.externalId,
32
+ stage: open[0]?.externalId,
33
+ amount: "1500.50",
34
+ currency: "EUR",
35
+ }, operation());
36
+ return { ...fixture, kind, open, made };
37
+ };
38
+ return [
39
+ {
40
+ name: "capabilities: known verbs and kinds, and a yes or no for repeated writes",
41
+ async run() {
42
+ const { writer } = await make();
43
+ const { kinds, verbs, idempotent } = writer.capabilities;
44
+ assert.ok(kinds.length > 0 && kinds.every((kind) => DEAL_KINDS.includes(kind)));
45
+ assert.ok(verbs.length > 0 && verbs.every((verb) => CRM_VERBS.includes(verb)));
46
+ assert.equal(typeof idempotent, "boolean");
47
+ },
48
+ },
49
+ {
50
+ name: "create answers the whole item in the stage it was given, with its new id",
51
+ async run() {
52
+ const { made, pipeline, open } = await openStages();
53
+ assert.ok(made.externalId, "an id");
54
+ assert.equal(made.title, "Example contract deal");
55
+ assert.equal(made.pipeline, pipeline.externalId);
56
+ assert.equal(made.stage, open[0]?.externalId);
57
+ assert.equal(made.currency, "EUR");
58
+ assert.equal(Number(made.amount), 1500.5);
59
+ },
60
+ },
61
+ {
62
+ name: "update of one field answers the whole item, every other field as it was",
63
+ async run() {
64
+ const { writer, kind, made } = await openStages();
65
+ const changed = await writer.update(kind, made.externalId, { title: "Example renamed" }, operation());
66
+ assert.equal(changed.externalId, made.externalId);
67
+ assert.equal(changed.title, "Example renamed");
68
+ for (const field of ["pipeline", "stage", "amount", "currency", "organization"])
69
+ assert.deepEqual(changed[field] ?? null, made[field] ?? null, `${field} kept`);
70
+ assert.deepEqual(changed.people ?? [], made.people ?? [], "people kept");
71
+ },
72
+ },
73
+ {
74
+ name: "move goes to the stage given; close marks it won",
75
+ async run() {
76
+ const { writer, kind, made, open, pipeline } = await openStages();
77
+ const moved = await writer.move(kind, made.externalId, open[1]?.externalId, operation());
78
+ assert.equal(moved.stage, open[1]?.externalId);
79
+ assert.equal(outcomeOf(pipeline, moved), "open");
80
+ const closed = await writer.close(kind, made.externalId, "won", operation());
81
+ assert.equal(outcomeOf(pipeline, closed), "won");
82
+ },
83
+ },
84
+ {
85
+ name: "remove answers once the CRM deleted it",
86
+ async run() {
87
+ const { writer, kind, made } = await openStages();
88
+ await writer.remove(kind, made.externalId, operation());
89
+ },
90
+ },
91
+ {
92
+ name: "a custom code that is a neutral field under another name is refused, and nothing is sent",
93
+ async run() {
94
+ const { writer, kind, made, aliases, requests, open } = await openStages();
95
+ assert.ok(aliases.length > 0, "the fixture names the CRM's own names for stage and owner");
96
+ for (const code of aliases) {
97
+ const before = requests();
98
+ await refusedWith("validation_error", () => writer.update(kind, made.externalId, { custom: { [code]: open[1]?.externalId } }, operation()));
99
+ assert.equal(requests(), before, `${code}: nothing sent`);
100
+ }
101
+ },
102
+ },
103
+ {
104
+ name: "a lost answer is outcome_unknown after one request, never retried",
105
+ async run() {
106
+ const fixture = await openStages();
107
+ const before = fixture.requests();
108
+ fixture.loseNextAnswer();
109
+ await refusedWith("outcome_unknown", () => fixture.writer.update(fixture.kind, fixture.made.externalId, { title: "Example lost" }, operation()));
110
+ assert.equal(fixture.requests(), before + 1, "one request");
111
+ },
112
+ },
113
+ {
114
+ name: "an idempotent CRM answers a repeated operation id from its record",
115
+ async run() {
116
+ const { writer, kind, pipeline } = await make();
117
+ if (!writer.capabilities.idempotent)
118
+ return;
119
+ const once = operation();
120
+ const draft = { title: "Example repeated", pipeline: pipeline.externalId };
121
+ const first = await writer.create(kind ?? "deal", draft, once);
122
+ const again = await writer.create(kind ?? "deal", draft, once);
123
+ assert.equal(again.externalId, first.externalId);
124
+ },
125
+ },
126
+ ];
127
+ };
@@ -0,0 +1,76 @@
1
+ import type { CrmItem } from "../domain/crm.js";
2
+ import type { DealKind, DealRole } from "../domain/deals.js";
3
+ import type { Level, PermissionKey } from "../sends/permissions.js";
4
+ export declare const CRM_VERBS: readonly ["create", "update", "move", "close", "remove"];
5
+ export type CrmVerb = (typeof CRM_VERBS)[number];
6
+ export interface CrmWriteCapabilities {
7
+ /** The kinds the CRM takes writes for. */
8
+ kinds: readonly DealKind[];
9
+ verbs: readonly CrmVerb[];
10
+ /**
11
+ * The CRM answers a repeat of an `operationId` from its record instead of doing the write again (Bitrix24 REST
12
+ * 3.0's `Idempotency-Key`). Only then may the owner repeat a write whose answer was lost with the same id.
13
+ */
14
+ idempotent: boolean;
15
+ }
16
+ /**
17
+ * A deal's or lead's fields in `CrmItem`'s names, each the CRM's own id; `null` clears one. `custom` holds the CRM's
18
+ * own fields by their code. A custom code that is one of the named fields under another name (a stage, a pipeline, an
19
+ * owner or a status) is refused by the adapter: through it a write would get round the keys those fields check.
20
+ */
21
+ export interface CrmFields {
22
+ title?: string;
23
+ pipeline?: string;
24
+ stage?: string;
25
+ /** A decimal string in `currency`; both or neither. */
26
+ amount?: string | null;
27
+ currency?: string | null;
28
+ /** `YYYY-MM-DD`. */
29
+ closeDate?: string | null;
30
+ organization?: string | null;
31
+ /** Replaces the whole list. */
32
+ people?: {
33
+ externalId: string;
34
+ role: DealRole;
35
+ }[];
36
+ /** The CRM user it is assigned to: a user's id, never a contact's. */
37
+ owner?: string | null;
38
+ custom?: Record<string, string | null>;
39
+ }
40
+ export type CrmDraft = CrmFields & {
41
+ title: string;
42
+ };
43
+ export interface CrmWriteOptions {
44
+ /** Journaled before the call; an idempotent CRM gets it as its idempotency key. */
45
+ operationId: string;
46
+ }
47
+ /**
48
+ * Writes to one CRM account. Each answers the whole item as the CRM holds it after the write, every field and not
49
+ * only the changed ones: the store replaces its copy with it. None of them retries: a write whose answer was lost
50
+ * throws `outcome_unknown`.
51
+ */
52
+ export interface CrmWriter {
53
+ readonly provider: string;
54
+ readonly capabilities: CrmWriteCapabilities;
55
+ create(kind: DealKind, draft: CrmDraft, options: CrmWriteOptions): Promise<CrmItem>;
56
+ update(kind: DealKind, externalId: string, patch: CrmFields, options: CrmWriteOptions): Promise<CrmItem>;
57
+ /** To `stage`, a stage of the item's own pipeline. */
58
+ move(kind: DealKind, externalId: string, stage: string, options: CrmWriteOptions): Promise<CrmItem>;
59
+ /**
60
+ * Won or lost, the CRM's own way: Pipedrive sets a status, Kommo and Bitrix24 move it to the pipeline's closing
61
+ * stage, HubSpot to a closed stage.
62
+ */
63
+ close(kind: DealKind, externalId: string, outcome: "won" | "lost", options: CrmWriteOptions & {
64
+ reason?: string;
65
+ }): Promise<CrmItem>;
66
+ remove(kind: DealKind, externalId: string, options: CrmWriteOptions): Promise<void>;
67
+ }
68
+ /** Giving a record to a CRM user other than the one the sign-in acts as. */
69
+ export declare const CRM_ASSIGN_OTHERS_KEY: PermissionKey;
70
+ /** The keys the CRM writes check beyond their command paths, for a CLI's `commandPermissions({ extra })`. */
71
+ export declare const CRM_WRITE_KEYS: readonly PermissionKey[];
72
+ /**
73
+ * Built in, and only ever tightening (WC-PERM-7): an ordinary change goes ahead, a deletion asks, and a record is not
74
+ * given to somebody else until the owner allows it.
75
+ */
76
+ export declare const CRM_WRITE_DEFAULTS: Readonly<Record<PermissionKey, Level>>;
@@ -0,0 +1,14 @@
1
+ export const CRM_VERBS = ["create", "update", "move", "close", "remove"];
2
+ /** Giving a record to a CRM user other than the one the sign-in acts as. */
3
+ export const CRM_ASSIGN_OTHERS_KEY = "crm.assign-others";
4
+ /** The keys the CRM writes check beyond their command paths, for a CLI's `commandPermissions({ extra })`. */
5
+ export const CRM_WRITE_KEYS = [CRM_ASSIGN_OTHERS_KEY];
6
+ /**
7
+ * Built in, and only ever tightening (WC-PERM-7): an ordinary change goes ahead, a deletion asks, and a record is not
8
+ * given to somebody else until the owner allows it.
9
+ */
10
+ export const CRM_WRITE_DEFAULTS = {
11
+ "deals.delete": "ask",
12
+ "leads.delete": "ask",
13
+ [CRM_ASSIGN_OTHERS_KEY]: "deny",
14
+ };
@@ -0,0 +1,68 @@
1
+ import type { DealKind } from "../domain/deals.js";
2
+ import type { AppIdentity } from "../ports/app.js";
3
+ import type { Warn } from "../runs/events.js";
4
+ import { type Asker, type GuardSettings } from "../sends/guard.js";
5
+ import type { Deal } from "../store/sqlite/deals.js";
6
+ import type { MessageStore } from "../store/store.js";
7
+ import type { AccountKey } from "../store/types.js";
8
+ import type { CrmSource } from "./source.js";
9
+ import { type CrmWriter } from "./writer.js";
10
+ /** What a CLI hands the CRM writes for one profile. */
11
+ export interface CrmWrites {
12
+ writer: CrmWriter;
13
+ app: AppIdentity;
14
+ /** The profile's levels, read on every write so a config change applies at once. */
15
+ settings: () => GuardSettings;
16
+ /** The CRM user the sign-in acts as. An owner other than it checks `crm.assign-others`; left out, every owner does. */
17
+ self?: string;
18
+ env?: NodeJS.ProcessEnv;
19
+ warn?: Warn;
20
+ }
21
+ export interface CrmWriteContext {
22
+ store: MessageStore;
23
+ account: AccountKey;
24
+ writes: CrmWrites;
25
+ /** A terminal's question, or an agent's go-ahead. */
26
+ ask: Asker;
27
+ /** Reads the kind's pipelines again when the CRM answers an item in a stage the store does not hold yet. */
28
+ source?: () => CrmSource;
29
+ /** How the caller spells the operation id in a hint: `--operation-id`, or `operation_id` over MCP. */
30
+ names?: {
31
+ id: string;
32
+ };
33
+ }
34
+ /** The fields as typed: a pipeline and a stage by reference or name, money as a decimal. */
35
+ export interface CrmFieldsInput {
36
+ title?: string;
37
+ pipeline?: string;
38
+ stage?: string;
39
+ amount?: string;
40
+ currency?: string;
41
+ closeDate?: string;
42
+ owner?: string;
43
+ custom?: Record<string, string | null>;
44
+ }
45
+ export interface CrmWritten {
46
+ operationId: string;
47
+ item: Deal;
48
+ }
49
+ export declare const createCrmItem: (context: CrmWriteContext, kind: DealKind, input: CrmFieldsInput, options?: {
50
+ operationId?: string;
51
+ }) => Promise<CrmWritten>;
52
+ export declare const updateCrmItem: (context: CrmWriteContext, kind: DealKind, reference: string, input: CrmFieldsInput, options?: {
53
+ operationId?: string;
54
+ }) => Promise<CrmWritten>;
55
+ export declare const moveCrmItem: (context: CrmWriteContext, kind: DealKind, reference: string, stageName: string, options?: {
56
+ operationId?: string;
57
+ }) => Promise<CrmWritten>;
58
+ export declare const closeCrmItem: (context: CrmWriteContext, kind: DealKind, reference: string, outcome: "won" | "lost", options?: {
59
+ reason?: string;
60
+ operationId?: string;
61
+ }) => Promise<CrmWritten>;
62
+ export declare const removeCrmItem: (context: CrmWriteContext, kind: DealKind, reference: string, options?: {
63
+ operationId?: string;
64
+ }) => Promise<{
65
+ operationId: string;
66
+ ref: string;
67
+ removed: number;
68
+ }>;