conduyt 1.5.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/index.js +214 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -39,6 +39,10 @@ conduyt search "acme corp" # search across the CRM
|
|
|
39
39
|
conduyt insights summary # run an AI insight query
|
|
40
40
|
conduyt privacy export <id> # GDPR data-portability export (owner/admin)
|
|
41
41
|
conduyt privacy forget <id> --confirm FORGET # GDPR erasure — IRREVERSIBLE, owner only
|
|
42
|
+
conduyt reply-capture status # inbound reply capture: state, DNS records, provider health
|
|
43
|
+
conduyt reply-capture setup # create the receiving subdomain (--domain optional)
|
|
44
|
+
conduyt reply-capture verify # promote it to live once DNS is confirmed
|
|
45
|
+
conduyt reply-capture remove --confirm REMOVE # remove capture — replies to already-sent email are lost
|
|
42
46
|
conduyt api GET /api/v1/companies # raw authenticated request (escape hatch)
|
|
43
47
|
conduyt config show # show resolved config (key masked)
|
|
44
48
|
```
|
package/dist/index.js
CHANGED
|
@@ -82,6 +82,8 @@ contacts
|
|
|
82
82
|
.option("--email <email>", "email")
|
|
83
83
|
.option("--phone <phone>", "phone")
|
|
84
84
|
.option("--company <company>", "company name (auto-creates/links a company)")
|
|
85
|
+
.option("--source <source>", "lead source (first-write-wins acquisition truth)")
|
|
86
|
+
.option("--master-status <status>", "lead lifecycle status: open|won|lost|abandoned|disqualified or an account custom (terminal statuses silence all outbound automation)")
|
|
85
87
|
.option("--json <json>", "full JSON body, merged over the flags above (for any field)")
|
|
86
88
|
.action(run(async (client, opts) => {
|
|
87
89
|
const body = {};
|
|
@@ -95,8 +97,23 @@ contacts
|
|
|
95
97
|
body.phone = opts.phone;
|
|
96
98
|
if (opts.company !== undefined)
|
|
97
99
|
body.company = opts.company;
|
|
100
|
+
if (opts.source !== undefined) {
|
|
101
|
+
if (opts.source.trim() === "")
|
|
102
|
+
throw new Error("--source was provided but is blank. Omit it or pass a source.");
|
|
103
|
+
if (opts.source.length > 100)
|
|
104
|
+
throw new Error("--source must be at most 100 characters.");
|
|
105
|
+
body.source = opts.source;
|
|
106
|
+
}
|
|
107
|
+
if (opts.masterStatus !== undefined) {
|
|
108
|
+
if (opts.masterStatus.trim() === "")
|
|
109
|
+
throw new Error("--master-status was provided but is blank. Omit it or pass a status.");
|
|
110
|
+
if (opts.masterStatus.length > 40)
|
|
111
|
+
throw new Error("--master-status must be at most 40 characters.");
|
|
112
|
+
body.masterStatus = opts.masterStatus;
|
|
113
|
+
}
|
|
98
114
|
if (opts.json !== undefined)
|
|
99
115
|
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
116
|
+
assertContactSource(body.source, { allowNull: false });
|
|
100
117
|
return client.post("/api/v1/contacts", body);
|
|
101
118
|
}));
|
|
102
119
|
contacts
|
|
@@ -107,6 +124,8 @@ contacts
|
|
|
107
124
|
.option("--email <email>", "email")
|
|
108
125
|
.option("--phone <phone>", "phone")
|
|
109
126
|
.option("--company <company>", "company name (auto-creates/links a company; pass '' to clear)")
|
|
127
|
+
.option("--source <source>", "correct the lead's first-touch source (normally set once at creation — update only to fix bad attribution; pass 'null' to clear)")
|
|
128
|
+
.option("--master-status <status>", "lead lifecycle status: open|won|lost|abandoned|disqualified or an account custom (terminal statuses silence all outbound automation)")
|
|
110
129
|
.option("--json <json>", "full JSON body, merged over the flags above (for any field)")
|
|
111
130
|
.action(run(async (client, id, opts) => {
|
|
112
131
|
assertUuid(id, "contact id");
|
|
@@ -121,10 +140,28 @@ contacts
|
|
|
121
140
|
body.phone = opts.phone;
|
|
122
141
|
if (opts.company !== undefined)
|
|
123
142
|
body.company = opts.company;
|
|
143
|
+
if (opts.source !== undefined) {
|
|
144
|
+
if (opts.source !== "null") {
|
|
145
|
+
if (opts.source.trim() === "") {
|
|
146
|
+
throw new Error("--source was provided but is blank. Omit it, or pass 'null' to clear the source.");
|
|
147
|
+
}
|
|
148
|
+
if (opts.source.length > 100)
|
|
149
|
+
throw new Error("--source must be at most 100 characters.");
|
|
150
|
+
}
|
|
151
|
+
body.source = opts.source === "null" ? null : opts.source;
|
|
152
|
+
}
|
|
153
|
+
if (opts.masterStatus !== undefined) {
|
|
154
|
+
if (opts.masterStatus.trim() === "")
|
|
155
|
+
throw new Error("--master-status was provided but is blank. Omit it or pass a status.");
|
|
156
|
+
if (opts.masterStatus.length > 40)
|
|
157
|
+
throw new Error("--master-status must be at most 40 characters.");
|
|
158
|
+
body.masterStatus = opts.masterStatus;
|
|
159
|
+
}
|
|
124
160
|
if (opts.json !== undefined)
|
|
125
161
|
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
126
162
|
if (Object.keys(body).length === 0)
|
|
127
163
|
throw new Error("Nothing to update. Pass at least one field flag or --json.");
|
|
164
|
+
assertContactSource(body.source, { allowNull: true });
|
|
128
165
|
return client.patch(`/api/v1/contacts/${encodeURIComponent(id)}`, body);
|
|
129
166
|
}));
|
|
130
167
|
contacts
|
|
@@ -271,6 +308,7 @@ deals
|
|
|
271
308
|
.option("--value <n>", "deal value (number)")
|
|
272
309
|
.option("--currency <code>", "ISO currency, e.g. GBP")
|
|
273
310
|
.option("--contact <id>", "contact UUID to link")
|
|
311
|
+
.option("--source <source>", "attribution source for THIS deal (the trigger that produced it, e.g. sms-reply, booking:demo)")
|
|
274
312
|
.option("--json <json>", "full JSON body, merged over the flags")
|
|
275
313
|
.action(run(async (client, opts) => {
|
|
276
314
|
const body = {};
|
|
@@ -295,6 +333,13 @@ deals
|
|
|
295
333
|
body.currency = opts.currency;
|
|
296
334
|
if (opts.contact !== undefined)
|
|
297
335
|
body.contactId = opts.contact;
|
|
336
|
+
if (opts.source !== undefined) {
|
|
337
|
+
if (opts.source.trim() === "")
|
|
338
|
+
throw new Error("--source was provided but is blank. Omit it or pass a source.");
|
|
339
|
+
if (opts.source.length > 500)
|
|
340
|
+
throw new Error("--source must be at most 500 characters.");
|
|
341
|
+
body.source = opts.source;
|
|
342
|
+
}
|
|
298
343
|
if (opts.json !== undefined)
|
|
299
344
|
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
300
345
|
assertFiniteValue(body.value);
|
|
@@ -317,6 +362,7 @@ deals
|
|
|
317
362
|
.option("--pipeline <id>", "move to this pipeline UUID")
|
|
318
363
|
.option("--contact <id>", "primary contact UUID")
|
|
319
364
|
.option("--status <status>", "explicit status (open|won|lost); overrides the status auto-derived from --stage")
|
|
365
|
+
.option("--source <source>", "attribution source for THIS deal (pass 'null' to clear it)")
|
|
320
366
|
.option("--json <json>", "full JSON body, merged over the flags")
|
|
321
367
|
.action(run(async (client, id, opts) => {
|
|
322
368
|
assertUuid(id, "deal id");
|
|
@@ -339,6 +385,16 @@ deals
|
|
|
339
385
|
body.contactId = opts.contact;
|
|
340
386
|
if (opts.status !== undefined)
|
|
341
387
|
body.status = opts.status;
|
|
388
|
+
if (opts.source !== undefined) {
|
|
389
|
+
// Mirror the API validator (blank rejected, 500-char cap) so the CLI
|
|
390
|
+
// fails locally rather than shipping a guaranteed 422.
|
|
391
|
+
if (opts.source.trim() === "") {
|
|
392
|
+
throw new Error("--source was provided but is blank. Omit it, or pass 'null' to clear the source.");
|
|
393
|
+
}
|
|
394
|
+
if (opts.source.length > 500)
|
|
395
|
+
throw new Error("--source must be at most 500 characters.");
|
|
396
|
+
body.source = opts.source === "null" ? null : opts.source;
|
|
397
|
+
}
|
|
342
398
|
if (opts.stage !== undefined) {
|
|
343
399
|
if (opts.stage.trim() === "")
|
|
344
400
|
throw new Error("--stage was provided but is blank. Omit it or pass a stage name/UUID.");
|
|
@@ -1604,6 +1660,147 @@ program
|
|
|
1604
1660
|
p = `/api/v1${p}`;
|
|
1605
1661
|
return client.request(method.toUpperCase(), p, body);
|
|
1606
1662
|
}));
|
|
1663
|
+
// ---- dialer ----
|
|
1664
|
+
const dialer = program.command("dialer").description("Dialer operations");
|
|
1665
|
+
dialer
|
|
1666
|
+
.command("sync-local-presence")
|
|
1667
|
+
.description("Reconcile the Local Presence pool from Twilio. Only adopts numbers whose Twilio FriendlyName contains a standalone 'LP' (or 'LocalPres'), that are voice-capable, and that are US +1 E.164 — first 50 sorted matches; pool entries that no longer match are REMOVED. Name the number in Twilio first, then sync. Requires settings:edit")
|
|
1668
|
+
.action(run(async (client) => client.post("/api/v1/dialer/local-presence/sync", {})));
|
|
1669
|
+
// ---- reply capture (inbound email) ----
|
|
1670
|
+
// The receiving subdomain that makes replies to CRM email record and thread
|
|
1671
|
+
// against the contact. Same lifecycle as the sending domain: setup -> DNS ->
|
|
1672
|
+
// verify -> live, and the name MUST be a strict subdomain of the account's own
|
|
1673
|
+
// VERIFIED sending domain (that rule is the cross-tenant fence, not
|
|
1674
|
+
// formatting). Several non-2xx outcomes here are NORMAL, not retry loops:
|
|
1675
|
+
// 409 "already configured" (remove first), 409 "contact support to reconcile"
|
|
1676
|
+
// (an operator-repair state reachable only through a super-admin session
|
|
1677
|
+
// endpoint — an API key deliberately cannot fix it), 429 (per-account budget:
|
|
1678
|
+
// DNS takes minutes), and 503 (the shared provider allowance is busy with
|
|
1679
|
+
// customer sends — says nothing about the domain).
|
|
1680
|
+
const replyCapture = program
|
|
1681
|
+
.command("reply-capture")
|
|
1682
|
+
.description("Inbound reply capture: the receiving subdomain that threads email replies to contacts");
|
|
1683
|
+
replyCapture
|
|
1684
|
+
.command("status")
|
|
1685
|
+
.description("Show reply-capture state: configured/live, the capture domain and reply address, pending DNS records, and provider health. health='checking' means the provider is re-checking DNS and capture stays active; degraded=true means it can no longer receive and must be removed and set up again; needsSupport=true means an operator must reconcile it; cleanupPending=true means a previous removal did not finish at the provider — re-run `remove` to complete it. After a removal, draining=true (with drainEndsAt) means the drain window is open — a domain that was live at removal keeps receiving replies until it closes (one that was degraded has no such guarantee); parked=true means it is retired and inbound-inert. Either way that name is permanently unavailable — use suggestedDomain when setting up again")
|
|
1686
|
+
.action(run(async (client) => client.get("/api/v1/email-domains/reply-capture")));
|
|
1687
|
+
replyCapture
|
|
1688
|
+
.command("setup")
|
|
1689
|
+
.description("Create the receiving subdomain at the email provider and return the DNS records to add. Requires a verified sending domain; the capture name must be a subdomain of it. Omit --domain to use the suggested reply.<sending-domain>. Add the records, then run `reply-capture verify`. A 409 can also mean the requested name belongs to a retired (drained/parked) domain — retired names are permanently unavailable, so use suggestedDomain from `reply-capture status` (e.g. reply2.<domain>) instead of retrying the old name")
|
|
1690
|
+
.option("--domain <domain>", "capture subdomain, e.g. reply.acme.com (must be a subdomain of your verified sending domain)")
|
|
1691
|
+
.action(run(async (client, opts) => {
|
|
1692
|
+
const domain = opts.domain?.trim();
|
|
1693
|
+
if (opts.domain !== undefined && !domain) {
|
|
1694
|
+
fail("--domain must not be blank. Omit it entirely to use the suggested reply.<sending-domain>.");
|
|
1695
|
+
}
|
|
1696
|
+
// Sent only when supplied — an absent domain is how the API is told to
|
|
1697
|
+
// use its suggestion.
|
|
1698
|
+
return client.post("/api/v1/email-domains/reply-capture", domain ? { domain } : {});
|
|
1699
|
+
}));
|
|
1700
|
+
replyCapture
|
|
1701
|
+
.command("verify")
|
|
1702
|
+
.description("Check the pending capture domain's DNS and promote it to live once the provider confirms inbound receiving. verified=false with DNS record states is the expected answer while records propagate — poll sparingly, this is budgeted per account")
|
|
1703
|
+
.action(run(async (client) => client.post("/api/v1/email-domains/reply-capture/verify", {})));
|
|
1704
|
+
replyCapture
|
|
1705
|
+
.command("remove")
|
|
1706
|
+
.description("Remove reply capture. Outcome depends on the domain's state. LIVE with a provider-backed setup: it DRAINS — the capture Reply-To stops on new email immediately, but the address keeps receiving for 30 days (drainEndsAt in the response) so late replies to already-sent emails still reach the contact's timeline; after that it parks, and the name is permanently retired (no account can ever set it up again — resuming capture means a NEW subdomain, see suggestedDomain on `reply-capture status`). LIVE legacy row with no stored provider id: removed immediately with NO drain — reply threading stops at once and the response says providerCleanup='manual' (an operator finishes the provider side); the name stays reusable. DEGRADED: drain lifecycle and permanent retirement, but receiving was already broken, so no 30-day delivery guarantee. PENDING setup, a domain the provider has already lost, or an interrupted-removal retry (cleanupPending): torn down immediately, the row is deleted, and the name stays reusable. Sending-domain removal stays blocked by every capture row except a released tombstone — pending/cleanup rows clear right here, but a draining/parked row keeps blocking until support releases it after the drain. Requires --confirm REMOVE")
|
|
1707
|
+
// A VALUE, not a boolean flag: `--confirm false` on a boolean would read as
|
|
1708
|
+
// true and delete anyway (same footgun the GDPR forget command guards).
|
|
1709
|
+
.allowExcessArguments(false)
|
|
1710
|
+
.option("--confirm <word>", "required — pass exactly REMOVE to authorize the removal (a provider-backed live/degraded name is permanently retired)")
|
|
1711
|
+
.action(run(async (client, opts) => {
|
|
1712
|
+
if (opts.confirm !== "REMOVE") {
|
|
1713
|
+
fail("Refusing to remove: pass --confirm REMOVE to authorize this. Sending stops immediately, and a provider-backed live or degraded domain's name is permanently retired — there is no undo on the retirement (a legacy id-less live row instead stops threading at once).");
|
|
1714
|
+
}
|
|
1715
|
+
return client.del("/api/v1/email-domains/reply-capture");
|
|
1716
|
+
}));
|
|
1717
|
+
// ---- lifecycle (master lead status + intake deals) ----
|
|
1718
|
+
const lifecycle = program
|
|
1719
|
+
.command("lifecycle")
|
|
1720
|
+
.description("Lifecycle v2 settings: lead master statuses and automatic intake-deal creation");
|
|
1721
|
+
lifecycle
|
|
1722
|
+
.command("settings")
|
|
1723
|
+
.description("Show the Lifecycle v2 configuration: masterStatuses.effective (the five defaults plus the account's customs) and intakeDeals (null can mean not configured OR not visible to your key — the setting is admin-visible only)")
|
|
1724
|
+
.action(run(async (client) => {
|
|
1725
|
+
const DEFAULTS = ["open", "won", "lost", "abandoned", "disqualified"];
|
|
1726
|
+
const result = (await client.get("/api/v1/settings"));
|
|
1727
|
+
const raw = result?.data ?? {};
|
|
1728
|
+
const customs = Array.isArray(raw.masterStatuses)
|
|
1729
|
+
? raw.masterStatuses.filter((x) => typeof x === "string")
|
|
1730
|
+
: [];
|
|
1731
|
+
const intakeDeals = raw.intakeDeals && typeof raw.intakeDeals === "object" ? raw.intakeDeals : null;
|
|
1732
|
+
return {
|
|
1733
|
+
data: {
|
|
1734
|
+
masterStatuses: {
|
|
1735
|
+
defaults: DEFAULTS,
|
|
1736
|
+
customs,
|
|
1737
|
+
effective: [...DEFAULTS, ...customs.filter((c) => !DEFAULTS.includes(c.toLowerCase()))],
|
|
1738
|
+
},
|
|
1739
|
+
intakeDeals,
|
|
1740
|
+
...(intakeDeals === null
|
|
1741
|
+
? {
|
|
1742
|
+
intakeDealsNote: "null = not configured, OR your key's role cannot read this admin-visible setting",
|
|
1743
|
+
}
|
|
1744
|
+
: {}),
|
|
1745
|
+
},
|
|
1746
|
+
};
|
|
1747
|
+
}));
|
|
1748
|
+
lifecycle
|
|
1749
|
+
.command("set-intake-deals")
|
|
1750
|
+
.description("Configure automatic deal creation on live intake (webhooks, public forms, public bookings). Even under --re-inbound always: bulk imports and staff/calendar/dialer-created appointments never mint, exact replays and same-source events within 10 min are skipped, and terminal-status contacts get no new deals")
|
|
1751
|
+
.option("--enable", "turn intake deals ON")
|
|
1752
|
+
.option("--disable", "turn intake deals OFF")
|
|
1753
|
+
.option("--pipeline <id>", "pipeline UUID the intake deals land in")
|
|
1754
|
+
.option("--stage <id>", "starting stage UUID within that pipeline")
|
|
1755
|
+
.option("--re-inbound <policy>", "policy for existing contacts: always|if_no_open|never")
|
|
1756
|
+
.action(run(async (client, opts) => {
|
|
1757
|
+
if (opts.enable && opts.disable)
|
|
1758
|
+
throw new Error("Pass either --enable or --disable, not both.");
|
|
1759
|
+
if (opts.enable === undefined && opts.disable === undefined) {
|
|
1760
|
+
throw new Error("Pass --enable or --disable.");
|
|
1761
|
+
}
|
|
1762
|
+
if (!opts.pipeline || !opts.stage) {
|
|
1763
|
+
// The API requires the full object, so the CLI must too — a partial
|
|
1764
|
+
// PATCH would 422 with a less obvious message.
|
|
1765
|
+
throw new Error("--pipeline and --stage are required (see `conduyt pipelines list`).");
|
|
1766
|
+
}
|
|
1767
|
+
const body = {
|
|
1768
|
+
enabled: Boolean(opts.enable),
|
|
1769
|
+
pipelineId: opts.pipeline,
|
|
1770
|
+
stageId: opts.stage,
|
|
1771
|
+
};
|
|
1772
|
+
if (opts.reInbound !== undefined) {
|
|
1773
|
+
if (!["always", "if_no_open", "never"].includes(opts.reInbound)) {
|
|
1774
|
+
throw new Error("--re-inbound must be one of: always, if_no_open, never.");
|
|
1775
|
+
}
|
|
1776
|
+
body.reInbound = opts.reInbound;
|
|
1777
|
+
}
|
|
1778
|
+
return client.patch("/api/v1/settings", { intakeDeals: body });
|
|
1779
|
+
}));
|
|
1780
|
+
lifecycle
|
|
1781
|
+
.command("set-master-statuses [statuses...]")
|
|
1782
|
+
.description("Set the account's CUSTOM lead lifecycle statuses (the five defaults always exist). Pass --clear with no names to remove all customs")
|
|
1783
|
+
.option("--clear", "remove ALL custom statuses (restore defaults-only)")
|
|
1784
|
+
.action(run(async (client, statuses, opts) => {
|
|
1785
|
+
statuses = statuses ?? [];
|
|
1786
|
+
if (statuses.length === 0 && !opts.clear) {
|
|
1787
|
+
throw new Error("Pass at least one status name, or --clear to remove all customs.");
|
|
1788
|
+
}
|
|
1789
|
+
if (statuses.length > 0 && opts.clear) {
|
|
1790
|
+
throw new Error("--clear cannot be combined with status names.");
|
|
1791
|
+
}
|
|
1792
|
+
// Mirror of the API validator: max 20 names, 1-40 chars, letters/
|
|
1793
|
+
// numbers/spaces/-/_ only — fail fast locally instead of a late 422.
|
|
1794
|
+
const clean = statuses.map((s) => s.trim());
|
|
1795
|
+
if (clean.some((s) => !s))
|
|
1796
|
+
throw new Error("Status names must not be blank.");
|
|
1797
|
+
if (clean.length > 20)
|
|
1798
|
+
throw new Error("At most 20 custom statuses.");
|
|
1799
|
+
const bad = clean.find((s) => s.length > 40 || !/^[a-z0-9][a-z0-9 _-]*$/i.test(s));
|
|
1800
|
+
if (bad)
|
|
1801
|
+
throw new Error(`Invalid status name '${bad}' — 1-40 chars; letters, numbers, spaces, - and _ only.`);
|
|
1802
|
+
return client.patch("/api/v1/settings", { masterStatuses: clean });
|
|
1803
|
+
}));
|
|
1607
1804
|
program.parseAsync(process.argv).catch(fail);
|
|
1608
1805
|
// helpers
|
|
1609
1806
|
// Parse a --json / --stages style argument, throwing a clear CLI error (caught
|
|
@@ -1761,6 +1958,23 @@ function run(handler) {
|
|
|
1761
1958
|
}
|
|
1762
1959
|
};
|
|
1763
1960
|
}
|
|
1961
|
+
// Contact source is the first-write-wins acquisition truth: a blank value
|
|
1962
|
+
// must never reach storage, whether it arrived via a flag or --json (the
|
|
1963
|
+
// merge runs last, so this validates the FINAL body).
|
|
1964
|
+
function assertContactSource(value, opts) {
|
|
1965
|
+
if (value === undefined)
|
|
1966
|
+
return;
|
|
1967
|
+
if (value === null) {
|
|
1968
|
+
if (opts.allowNull)
|
|
1969
|
+
return;
|
|
1970
|
+
throw new Error("source cannot be null on create — omit it instead.");
|
|
1971
|
+
}
|
|
1972
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
1973
|
+
throw new Error("source must be a non-blank string (or omitted).");
|
|
1974
|
+
}
|
|
1975
|
+
if (value.length > 100)
|
|
1976
|
+
throw new Error("source must be at most 100 characters.");
|
|
1977
|
+
}
|
|
1764
1978
|
function mask(key) {
|
|
1765
1979
|
if (!key)
|
|
1766
1980
|
return "(not set)";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conduyt",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Command-line interface for Conduyt CRM
|
|
3
|
+
"version": "1.8.0",
|
|
4
|
+
"description": "Command-line interface for Conduyt CRM — manage contacts, deals, pipelines, and run insight queries from your terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
"build": "tsc",
|
|
19
19
|
"typecheck": "tsc --noEmit",
|
|
20
20
|
"start": "tsx src/index.ts",
|
|
21
|
-
"prepublishOnly": "npm run build",
|
|
22
|
-
"test": "node --import tsx --test src/*.test.ts"
|
|
21
|
+
"prepublishOnly": "npm run check:version && npm run build && node --import tsx --experimental-test-module-mocks --test src/privacy-forget.test.ts src/contact-source-json.test.ts",
|
|
22
|
+
"test": "node --import tsx --test src/*.test.ts",
|
|
23
|
+
"check:version": "node -e \"const p=require('./package.json').version,l=require('./package-lock.json');if(l.version!==p||l.packages[''].version!==p){console.error('lockfile version != '+p);process.exit(1)}console.log('Version sync OK: '+p)\""
|
|
23
24
|
},
|
|
24
25
|
"keywords": [
|
|
25
26
|
"conduyt",
|