@wirecat/cli-core 0.596.0 → 0.597.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/dist/background/deliver.d.ts +7 -1
- package/dist/background/deliver.js +66 -10
- package/dist/background/telegram-html.d.ts +2 -0
- package/dist/background/telegram-html.js +66 -0
- package/dist/calendar/mcp/index.d.ts +12 -12
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/permission-keys.d.ts +10 -2
- package/dist/cli/preview.d.ts +3 -0
- package/dist/cli/preview.js +9 -5
- package/dist/crm/cli/index.js +71 -14
- package/dist/crm/index.d.ts +1 -1
- package/dist/crm/index.js +1 -1
- package/dist/crm/source.d.ts +6 -1
- package/dist/crm/sync.d.ts +5 -4
- package/dist/crm/sync.js +22 -4
- package/dist/crm/testing/index.d.ts +1 -1
- package/dist/crm/testing/index.js +1 -1
- package/dist/crm/testing/source-contract.js +28 -0
- package/dist/crm/testing/source.d.ts +8 -1
- package/dist/crm/testing/source.js +53 -9
- package/dist/crm/testing/sync-contract.js +14 -3
- package/dist/crm/writer.d.ts +11 -2
- package/dist/crm/writer.js +31 -0
- package/dist/crm/writes.d.ts +12 -2
- package/dist/crm/writes.js +93 -8
- package/dist/domain/crm.d.ts +29 -1
- package/dist/entities/deal.d.ts +16 -0
- package/dist/entities/deal.js +8 -0
- package/dist/entities/index.d.ts +12 -0
- package/dist/errors/reasons.js +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/store/driver.d.ts +2 -0
- package/dist/store/driver.js +2 -0
- package/dist/store/search-index.js +6 -1
- package/dist/store/sqlite/accounts.js +1 -0
- package/dist/store/sqlite/crm.d.ts +5 -1
- package/dist/store/sqlite/crm.js +52 -0
- package/dist/store/sqlite/deals.d.ts +5 -0
- package/dist/store/sqlite/deals.js +13 -0
- package/dist/store/sqlite/manifest.js +1 -0
- package/dist/store/sqlite/migrations.generated.js +6 -0
- package/dist/store/sqlite/note-index.d.ts +1 -1
- package/dist/store/sqlite/note-index.js +3 -0
- package/dist/store/sqlite/schema.d.ts +217 -0
- package/dist/store/sqlite/schema.js +18 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,56 @@ 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.597.0 — 07.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`--field` by label.** CRM `create` and `update` resolve `--field <key>=<value>` by the synced field definitions:
|
|
15
|
+
a field by its code or whole label (a label two fields share is refused as `field.ambiguous` with both codes, an
|
|
16
|
+
unknown one as `field.unknown` with close candidates), a list field's option by its id or label
|
|
17
|
+
(`field.unknown-option`), and a multiple field's repeats into a list; a single field given twice is refused. A CRM
|
|
18
|
+
whose definitions are not stored takes codes as typed.
|
|
19
|
+
- **`CrmWrites.alsoChecks(kind, verb)`**: a CLI's own keys a write also checks, such as `api.crm.deal.update`.
|
|
20
|
+
`crmAlsoChecks(also)` gives `commandPermissions({ alsoChecks })` every key a CRM write may check, so `--dry-run`
|
|
21
|
+
names the implied move, close and `crm.assign-others` keys and the CLI's own. A key an argument implies is a
|
|
22
|
+
`ConditionalKey` `{ key, when }`: `commandPermissions({ alsoChecks })` may return one, and the preview shows its level
|
|
23
|
+
and `when` but never refuses on it.
|
|
24
|
+
- **A deal's and lead's `provider_metadata` and `assignee`.** The CRM's own field values, by code as
|
|
25
|
+
`{ label, value, option_id? }` (`CrmValue`), kept as stored in `metadata`, codes never renamed; `CrmItem.assignee`, the
|
|
26
|
+
CRM user it is assigned to, answered as the identity `<account>/user/<id>`.
|
|
27
|
+
- **CRM field definitions (store version 54).** `CrmSource.fields?(kind)` lists a CRM's own fields on deals, leads,
|
|
28
|
+
contacts or companies: `CrmField` `{ code, label, type, multiple, filterable, options? }`. `syncCrm` reads them every
|
|
29
|
+
run, after the pipelines, and `store.crm.saveFields` keeps each kind's whole list in `crm_fields`: one the CRM no
|
|
30
|
+
longer lists is marked deleted, and an empty list marks them all. `store.crm.fields(account, kind)` reads them in
|
|
31
|
+
the CRM's order. Values stay in `metadata`, keyed by code. The migration adds one table; `minCompatible` stays 34.
|
|
32
|
+
- **`fields list <deals|leads|people|organizations>`** in `addCrmCommands`: the synced fields of one kind, with
|
|
33
|
+
`--limit`, `--page` and `--all`, in bitrix24-cli's `fields list` shape plus `untrusted`. An unknown kind is refused
|
|
34
|
+
as `field.unknown-kind` with the allowed words.
|
|
35
|
+
- **`crmSourceContract` checks `fields()`** when an adapter has it, and `crmSyncContract` checks they are stored. The
|
|
36
|
+
fake CRM has `setFields`, `calls.fields` and `SOURCE_FIELD` on its deals.
|
|
37
|
+
- **A schedule's Telegram delivery is Telegram HTML.** `--to telegram-bot:<profile>/<chat>` converts the report's
|
|
38
|
+
Markdown (headings, bold, italic, code, links, lists, tables as label lines, rules) to the tags `tg` accepts and
|
|
39
|
+
sends it with `--html`; the 4096 and 1024 character limits count the converted text, and a long report's caption
|
|
40
|
+
is cut until its HTML fits. When `tg` answers `validation_error`, the send is repeated once as plain text and
|
|
41
|
+
recorded as `delivery.html-fallback`; any other failure is never resent. Email stays plain text.
|
|
42
|
+
|
|
43
|
+
### Changed — may break callers
|
|
44
|
+
|
|
45
|
+
- **`CrmFieldsInput.custom` is the `--field` pairs as typed** (`[name, value][]`), and `CrmFields.custom` values may be
|
|
46
|
+
a list for a multiple field.
|
|
47
|
+
- **A write the CRM made whose item could not be read back is journaled as sent.** A `CrmWriter` throws it with
|
|
48
|
+
`sent: true` in the details; the error is answered with the write's `operation_id`, no longer journaled as failed.
|
|
49
|
+
- **A CRM sync with `fields()` makes one more request per kind**, so a `--max-pages` budget reaches the items later.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **A search over a read-only store no longer fails with "attempt to write a readonly database".** The message index
|
|
54
|
+
top-up and the notes, mail, events, memories and tasks index drains are skipped on a connection with `query_only`
|
|
55
|
+
(every read-only open), so `search all` in a CRM CLI answers from every source of a store only a CRM sync wrote. They
|
|
56
|
+
run on the next writable open.
|
|
57
|
+
|
|
8
58
|
## 0.596.0 — 06.11.2026
|
|
9
59
|
|
|
10
60
|
### Added
|
|
@@ -22,9 +22,13 @@ export declare const DELIVERY_REASONS: {
|
|
|
22
22
|
readonly routeInvalid: "delivery.route-invalid";
|
|
23
23
|
readonly timeout: "delivery.timeout";
|
|
24
24
|
readonly failed: "delivery.failed";
|
|
25
|
+
readonly htmlFallback: "delivery.html-fallback";
|
|
25
26
|
};
|
|
26
27
|
export type DeliveryReason = (typeof DELIVERY_REASONS)[keyof typeof DELIVERY_REASONS];
|
|
27
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* One route's outcome, kept in `last.json`. `error` is the sending CLI's error code and reason id, never its message.
|
|
30
|
+
* A send that went as plain text because Telegram refused the HTML is `ok` with the `html-fallback` reason.
|
|
31
|
+
*/
|
|
28
32
|
export interface Delivery {
|
|
29
33
|
to: string;
|
|
30
34
|
ok: boolean;
|
|
@@ -46,6 +50,8 @@ export interface Parcel {
|
|
|
46
50
|
/** A failed run: one line, no file. */
|
|
47
51
|
line?: string;
|
|
48
52
|
}
|
|
53
|
+
/** The caption as HTML: the head is cut in Markdown, where no tag can be split, and cut shorter until its HTML fits. */
|
|
54
|
+
export declare const htmlHeadOf: (title: string, report: string, limit?: number) => string;
|
|
49
55
|
/** Hands the parcel to each route's CLI, one after another; a route that fails is recorded and the next one still goes. */
|
|
50
56
|
export declare const deliver: (system: ServerSystem, env: NodeJS.ProcessEnv, routes: readonly string[], parcel: Parcel, { find, errorOf, locked, timeoutMs, }: {
|
|
51
57
|
/** Where a program is on the run's PATH. */
|
|
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { CliError } from "@wirecat/cli-base";
|
|
5
5
|
import { TIMED_OUT } from "./system.js";
|
|
6
|
+
import { toTelegramHtml } from "./telegram-html.js";
|
|
6
7
|
const PROGRAMS = {
|
|
7
8
|
"telegram-bot": { command: "tg", prefix: "TG" },
|
|
8
9
|
email: { command: "mail", prefix: "MAIL" },
|
|
@@ -36,6 +37,7 @@ export const DELIVERY_REASONS = {
|
|
|
36
37
|
routeInvalid: "delivery.route-invalid",
|
|
37
38
|
timeout: "delivery.timeout",
|
|
38
39
|
failed: "delivery.failed",
|
|
40
|
+
htmlFallback: "delivery.html-fallback",
|
|
39
41
|
};
|
|
40
42
|
/** Telegram's limits, in UTF-16 units as JavaScript counts them: a message's text, and a file's caption. */
|
|
41
43
|
export const TELEGRAM_TEXT_CHARS = 4096;
|
|
@@ -52,17 +54,61 @@ export const headOf = (title, report, limit = CAPTION_CHARS) => {
|
|
|
52
54
|
const line = cut.lastIndexOf("\n");
|
|
53
55
|
return `${title}\n${line > room / 2 ? cut.slice(0, line) : cut}${more}`;
|
|
54
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* `tg` answers a 400 from Telegram, which is what a refused HTML text is, as `validation_error`, and says nothing
|
|
59
|
+
* more exact. Any other code (network_error, outcome_unknown, provider_error…) may mean the message arrived.
|
|
60
|
+
*/
|
|
61
|
+
const REFUSED_CONTENT = "validation_error";
|
|
62
|
+
const codeOf = (stderr) => {
|
|
63
|
+
for (const line of stderr.split("\n").reverse()) {
|
|
64
|
+
try {
|
|
65
|
+
const { code } = JSON.parse(line).error ?? {};
|
|
66
|
+
if (typeof code === "string")
|
|
67
|
+
return code;
|
|
68
|
+
}
|
|
69
|
+
catch { }
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const textOf = (title, report) => (report.trim() ? `${title}\n${report.trim()}` : title);
|
|
73
|
+
/** The same text as Telegram HTML; the length that counts against the limits is this one. */
|
|
74
|
+
const htmlOf = (title, report) => toTelegramHtml(textOf(title, report));
|
|
75
|
+
/** The caption as HTML: the head is cut in Markdown, where no tag can be split, and cut shorter until its HTML fits. */
|
|
76
|
+
export const htmlHeadOf = (title, report, limit = CAPTION_CHARS) => {
|
|
77
|
+
let room = limit;
|
|
78
|
+
for (;;) {
|
|
79
|
+
const html = toTelegramHtml(headOf(title, report, room));
|
|
80
|
+
if (html.length <= limit || room <= title.length + 40)
|
|
81
|
+
return html.slice(0, limit);
|
|
82
|
+
room -= Math.max(html.length - limit, 1);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
55
85
|
/**
|
|
56
86
|
* The words that send along `route`. A bot sends a report that fits one message as its text, and a longer one as a
|
|
57
|
-
* file with its head as the caption
|
|
58
|
-
* with a dash stays text.
|
|
87
|
+
* file with its head as the caption, both as Telegram HTML unless `plain`; mail takes the text on stdin. The text goes
|
|
88
|
+
* after `--`, so a report that starts with a dash stays text.
|
|
59
89
|
*/
|
|
60
|
-
const argvOf = (program, route, title, report, copy) => {
|
|
90
|
+
const argvOf = (program, route, title, report, copy, plain = false) => {
|
|
61
91
|
const lead = [program, route.profile, "--json", "--no-input"];
|
|
62
92
|
if (route.kind === "email")
|
|
63
93
|
return [...lead, "messages", "send", "--to", route.address, "--subject", title];
|
|
64
|
-
const text =
|
|
65
|
-
|
|
94
|
+
const text = plain
|
|
95
|
+
? copy
|
|
96
|
+
? headOf(title, report)
|
|
97
|
+
: headOf(title, report, TELEGRAM_TEXT_CHARS)
|
|
98
|
+
: copy
|
|
99
|
+
? htmlHeadOf(title, report)
|
|
100
|
+
: htmlOf(title, report);
|
|
101
|
+
return [
|
|
102
|
+
...lead,
|
|
103
|
+
"bot",
|
|
104
|
+
"messages",
|
|
105
|
+
"send",
|
|
106
|
+
...(copy ? ["--file", copy] : []),
|
|
107
|
+
...(plain ? [] : ["--html"]),
|
|
108
|
+
"--",
|
|
109
|
+
route.chat,
|
|
110
|
+
text,
|
|
111
|
+
];
|
|
66
112
|
};
|
|
67
113
|
/** Hands the parcel to each route's CLI, one after another; a route that fails is recorded and the next one still goes. */
|
|
68
114
|
export const deliver = async (system, env, routes, parcel, { find, errorOf, locked, timeoutMs = DELIVERY_TIMEOUT_MS, }) => {
|
|
@@ -94,7 +140,7 @@ export const deliver = async (system, env, routes, parcel, { find, errorOf, lock
|
|
|
94
140
|
out.push(failed(to, DELIVERY_REASONS.programMissing, command));
|
|
95
141
|
continue;
|
|
96
142
|
}
|
|
97
|
-
const tooLong =
|
|
143
|
+
const tooLong = route.kind === "telegram-bot" && htmlOf(parcel.title, report).length > TELEGRAM_TEXT_CHARS;
|
|
98
144
|
// The sending CLI refuses its own and the other WireCat folders, where the report lives, so it gets a copy.
|
|
99
145
|
const folder = route.kind === "telegram-bot" && parcel.file && tooLong ? mkdtempSync(join(tmpdir(), "wirecat-deliver-")) : "";
|
|
100
146
|
const copy = folder && parcel.file ? join(folder, parcel.file.name) : undefined;
|
|
@@ -102,12 +148,22 @@ export const deliver = async (system, env, routes, parcel, { find, errorOf, lock
|
|
|
102
148
|
if (copy && parcel.file)
|
|
103
149
|
copyFileSync(parcel.file.path, copy);
|
|
104
150
|
const { [`${prefix}_PROFILE`]: _, ...childEnv } = env;
|
|
105
|
-
const
|
|
151
|
+
const options = {
|
|
106
152
|
timeoutMs,
|
|
107
153
|
...(route.kind === "email" ? { input: report.trim() ? report : parcel.title } : {}),
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
154
|
+
};
|
|
155
|
+
let ran = await system.run(argvOf(program, route, parcel.title, report, copy), childEnv, options);
|
|
156
|
+
let fellBack = "";
|
|
157
|
+
if (route.kind === "telegram-bot" &&
|
|
158
|
+
ran.code !== 0 &&
|
|
159
|
+
ran.code !== TIMED_OUT &&
|
|
160
|
+
codeOf(ran.stderr) === REFUSED_CONTENT) {
|
|
161
|
+
fellBack = errorOf(ran.stderr, ran.code);
|
|
162
|
+
ran = await system.run(argvOf(program, route, parcel.title, report, copy, true), childEnv, options);
|
|
163
|
+
}
|
|
164
|
+
if (ran.code === 0) {
|
|
165
|
+
out.push(fellBack ? { to, ok: true, reason: DELIVERY_REASONS.htmlFallback, error: fellBack } : { to, ok: true });
|
|
166
|
+
}
|
|
111
167
|
else if (ran.code === TIMED_OUT)
|
|
112
168
|
out.push(failed(to, DELIVERY_REASONS.timeout));
|
|
113
169
|
else
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const esc = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2
|
+
const SLOT = "\u0000";
|
|
3
|
+
/** Inline Markdown to the tags Telegram's HTML accepts; code and links are set aside first so nothing inside them is touched. */
|
|
4
|
+
const inline = (text) => {
|
|
5
|
+
const kept = [];
|
|
6
|
+
const keep = (html) => `${SLOT}${kept.push(html) - 1}${SLOT}`;
|
|
7
|
+
const done = esc(text.replaceAll(SLOT, ""))
|
|
8
|
+
.replace(/`([^`\n]+)`/g, (_, code) => keep(`<code>${code}</code>`))
|
|
9
|
+
.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)"]+)\)/g, (_, label, url) => keep(`<a href="${url}">${label}</a>`))
|
|
10
|
+
.replace(/\*\*(?=\S)(.+?)(?<=\S)\*\*|__(?=\S)(.+?)(?<=\S)__/g, (_, a, b) => `<b>${a ?? b}</b>`)
|
|
11
|
+
.replace(/(?<![\w*])\*(?=[^\s*])([^*\n]+?)(?<=[^\s*])\*(?![\w*])/g, "<i>$1</i>")
|
|
12
|
+
.replace(/(?<!\w)_(?=[^\s_])([^_\n]+?)(?<=[^\s_])_(?!\w)/g, "<i>$1</i>");
|
|
13
|
+
return done.replace(new RegExp(`${SLOT}(\\d+)${SLOT}`, "g"), (_, at) => kept[Number(at)] ?? "");
|
|
14
|
+
};
|
|
15
|
+
const cells = (row) => row
|
|
16
|
+
.trim()
|
|
17
|
+
.replace(/^\||\|$/g, "")
|
|
18
|
+
.split("|")
|
|
19
|
+
.map((cell) => cell.trim());
|
|
20
|
+
const isRule = (row) => /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(row);
|
|
21
|
+
/** A table has no Telegram form: each row becomes a bullet, its first cell the label and the rest `header: value`. */
|
|
22
|
+
const tableLines = (rows) => {
|
|
23
|
+
const [head = [], ...body] = rows.filter((row) => !isRule(row)).map(cells);
|
|
24
|
+
return body.map(([first = "", ...rest]) => {
|
|
25
|
+
const parts = rest.map((cell, at) => {
|
|
26
|
+
const name = head[at + 1];
|
|
27
|
+
return name && rest.length > 1 ? `${name}: ${inline(cell)}` : inline(cell);
|
|
28
|
+
});
|
|
29
|
+
return `• ${parts.length ? `<b>${inline(first)}</b>: ${parts.join(" · ")}` : inline(first)}`;
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
/** The report's common Markdown as Telegram HTML (<b>, <i>, <a>, <code>, <pre>); everything else is escaped text. */
|
|
33
|
+
export const toTelegramHtml = (markdown) => {
|
|
34
|
+
const out = [];
|
|
35
|
+
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
36
|
+
for (let at = 0; at < lines.length; at += 1) {
|
|
37
|
+
const line = lines[at] ?? "";
|
|
38
|
+
if (/^\s*```/.test(line)) {
|
|
39
|
+
const code = [];
|
|
40
|
+
at += 1;
|
|
41
|
+
while (at < lines.length && !/^\s*```/.test(lines[at] ?? ""))
|
|
42
|
+
code.push(lines[at++] ?? "");
|
|
43
|
+
out.push(`<pre>${esc(code.join("\n"))}</pre>`);
|
|
44
|
+
}
|
|
45
|
+
else if (/^\s*\|.*\|\s*$/.test(line)) {
|
|
46
|
+
const rows = [line];
|
|
47
|
+
while (/^\s*\|.*\|\s*$/.test(lines[at + 1] ?? "")) {
|
|
48
|
+
at += 1;
|
|
49
|
+
rows.push(lines[at] ?? "");
|
|
50
|
+
}
|
|
51
|
+
for (const row of tableLines(rows))
|
|
52
|
+
out.push(row);
|
|
53
|
+
}
|
|
54
|
+
else if (/^#{1,6}\s+\S/.test(line)) {
|
|
55
|
+
out.push(`<b>${inline(line.replace(/^#{1,6}\s+/, "").replace(/\*\*|__/g, "")).trimEnd()}</b>`);
|
|
56
|
+
}
|
|
57
|
+
else if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
|
|
58
|
+
out.push("──────");
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const bullet = /^(\s*)[-*+]\s+(.*)$/.exec(line);
|
|
62
|
+
out.push(bullet ? `${bullet[1]}• ${inline(bullet[2] ?? "")}` : inline(line));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out.join("\n");
|
|
66
|
+
};
|
|
@@ -10,13 +10,13 @@ export declare const calendarTools: ({
|
|
|
10
10
|
inputSchema: {
|
|
11
11
|
type: string;
|
|
12
12
|
properties: {
|
|
13
|
-
limit?: undefined;
|
|
14
13
|
account_id: {
|
|
15
14
|
type: string;
|
|
16
15
|
minimum: number;
|
|
17
16
|
};
|
|
18
17
|
query?: undefined;
|
|
19
18
|
timezone?: undefined;
|
|
19
|
+
limit?: undefined;
|
|
20
20
|
page?: undefined;
|
|
21
21
|
event?: undefined;
|
|
22
22
|
} | {
|
|
@@ -45,9 +45,9 @@ export declare const calendarTools: ({
|
|
|
45
45
|
};
|
|
46
46
|
event?: undefined;
|
|
47
47
|
} | {
|
|
48
|
-
limit?: undefined;
|
|
49
48
|
query?: undefined;
|
|
50
49
|
timezone?: undefined;
|
|
50
|
+
limit?: undefined;
|
|
51
51
|
page?: undefined;
|
|
52
52
|
event: {
|
|
53
53
|
type: string;
|
|
@@ -58,9 +58,9 @@ export declare const calendarTools: ({
|
|
|
58
58
|
minimum: number;
|
|
59
59
|
};
|
|
60
60
|
} | {
|
|
61
|
-
limit?: undefined;
|
|
62
61
|
query?: undefined;
|
|
63
62
|
timezone?: undefined;
|
|
63
|
+
limit?: undefined;
|
|
64
64
|
page?: undefined;
|
|
65
65
|
event?: undefined;
|
|
66
66
|
account_id?: undefined;
|
|
@@ -80,13 +80,13 @@ export declare const calendarTools: ({
|
|
|
80
80
|
inputSchema: {
|
|
81
81
|
type: string;
|
|
82
82
|
properties: {
|
|
83
|
-
limit?: undefined;
|
|
84
83
|
account_id: {
|
|
85
84
|
type: string;
|
|
86
85
|
minimum: number;
|
|
87
86
|
};
|
|
88
87
|
query?: undefined;
|
|
89
88
|
timezone?: undefined;
|
|
89
|
+
limit?: undefined;
|
|
90
90
|
page?: undefined;
|
|
91
91
|
event?: undefined;
|
|
92
92
|
} | {
|
|
@@ -115,9 +115,9 @@ export declare const calendarTools: ({
|
|
|
115
115
|
};
|
|
116
116
|
event?: undefined;
|
|
117
117
|
} | {
|
|
118
|
-
limit?: undefined;
|
|
119
118
|
query?: undefined;
|
|
120
119
|
timezone?: undefined;
|
|
120
|
+
limit?: undefined;
|
|
121
121
|
page?: undefined;
|
|
122
122
|
event: {
|
|
123
123
|
type: string;
|
|
@@ -128,9 +128,9 @@ export declare const calendarTools: ({
|
|
|
128
128
|
minimum: number;
|
|
129
129
|
};
|
|
130
130
|
} | {
|
|
131
|
-
limit?: undefined;
|
|
132
131
|
query?: undefined;
|
|
133
132
|
timezone?: undefined;
|
|
133
|
+
limit?: undefined;
|
|
134
134
|
page?: undefined;
|
|
135
135
|
event?: undefined;
|
|
136
136
|
account_id?: undefined;
|
|
@@ -150,13 +150,13 @@ export declare const calendarTools: ({
|
|
|
150
150
|
inputSchema: {
|
|
151
151
|
type: string;
|
|
152
152
|
properties: {
|
|
153
|
-
limit?: undefined;
|
|
154
153
|
account_id: {
|
|
155
154
|
type: string;
|
|
156
155
|
minimum: number;
|
|
157
156
|
};
|
|
158
157
|
query?: undefined;
|
|
159
158
|
timezone?: undefined;
|
|
159
|
+
limit?: undefined;
|
|
160
160
|
page?: undefined;
|
|
161
161
|
event?: undefined;
|
|
162
162
|
} | {
|
|
@@ -185,9 +185,9 @@ export declare const calendarTools: ({
|
|
|
185
185
|
};
|
|
186
186
|
event?: undefined;
|
|
187
187
|
} | {
|
|
188
|
-
limit?: undefined;
|
|
189
188
|
query?: undefined;
|
|
190
189
|
timezone?: undefined;
|
|
190
|
+
limit?: undefined;
|
|
191
191
|
page?: undefined;
|
|
192
192
|
event: {
|
|
193
193
|
type: string;
|
|
@@ -198,9 +198,9 @@ export declare const calendarTools: ({
|
|
|
198
198
|
minimum: number;
|
|
199
199
|
};
|
|
200
200
|
} | {
|
|
201
|
-
limit?: undefined;
|
|
202
201
|
query?: undefined;
|
|
203
202
|
timezone?: undefined;
|
|
203
|
+
limit?: undefined;
|
|
204
204
|
page?: undefined;
|
|
205
205
|
event?: undefined;
|
|
206
206
|
account_id?: undefined;
|
|
@@ -220,13 +220,13 @@ export declare const calendarTools: ({
|
|
|
220
220
|
inputSchema: {
|
|
221
221
|
type: string;
|
|
222
222
|
properties: {
|
|
223
|
-
limit?: undefined;
|
|
224
223
|
account_id: {
|
|
225
224
|
type: string;
|
|
226
225
|
minimum: number;
|
|
227
226
|
};
|
|
228
227
|
query?: undefined;
|
|
229
228
|
timezone?: undefined;
|
|
229
|
+
limit?: undefined;
|
|
230
230
|
page?: undefined;
|
|
231
231
|
event?: undefined;
|
|
232
232
|
} | {
|
|
@@ -255,9 +255,9 @@ export declare const calendarTools: ({
|
|
|
255
255
|
};
|
|
256
256
|
event?: undefined;
|
|
257
257
|
} | {
|
|
258
|
-
limit?: undefined;
|
|
259
258
|
query?: undefined;
|
|
260
259
|
timezone?: undefined;
|
|
260
|
+
limit?: undefined;
|
|
261
261
|
page?: undefined;
|
|
262
262
|
event: {
|
|
263
263
|
type: string;
|
|
@@ -268,9 +268,9 @@ export declare const calendarTools: ({
|
|
|
268
268
|
minimum: number;
|
|
269
269
|
};
|
|
270
270
|
} | {
|
|
271
|
-
limit?: undefined;
|
|
272
271
|
query?: undefined;
|
|
273
272
|
timezone?: undefined;
|
|
273
|
+
limit?: undefined;
|
|
274
274
|
page?: undefined;
|
|
275
275
|
event?: undefined;
|
|
276
276
|
account_id?: undefined;
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -115,7 +115,7 @@ export { type Saving, stored } from "./messenger/stored.js";
|
|
|
115
115
|
export { dateOrNone } from "./messenger/tasks-command.js";
|
|
116
116
|
export { MODEL_CATALOGUE_QUERY, type ModelCatalogueOptions } from "./model-catalogue-command.js";
|
|
117
117
|
export { positiveCount, window, withPaging } from "./paging.js";
|
|
118
|
-
export { type CommandPermissions, commandPathKey, commandPermissions, knownBeside, knownPermissionKeys, type PermissionKeyOf, type PermissionKeySource, refuseUnknownPermissionKeys, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
|
|
118
|
+
export { type CommandPermissions, type ConditionalKey, commandPathKey, commandPermissions, knownBeside, knownPermissionKeys, type PermissionKeyOf, type PermissionKeySource, refuseUnknownPermissionKeys, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
|
|
119
119
|
export { asFirstWord, commandWords, liftProfile, profileFree, refuseCommandName, rootOf, takesProfile, usableProfileName, } from "./profile.js";
|
|
120
120
|
export { createProgram, type ProgramDefinition, type ProgramOptions, type RunOptions, run } from "./program.js";
|
|
121
121
|
export { type ProjectTextHints, projectAnswerParts, projectCutoffTime, projectEvidenceText, projectWindowOf, withSyncCommands, } from "./project-evidence.js";
|
|
@@ -14,8 +14,16 @@ export interface CommandPermissions {
|
|
|
14
14
|
extra: readonly PermissionKey[];
|
|
15
15
|
/** cli-core's built-in levels and the CLI's own; they only ever tighten. */
|
|
16
16
|
defaults: Readonly<Record<PermissionKey, Level>>;
|
|
17
|
-
/**
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The other keys a command's real run checks, so its `--dry-run` preview shows each: gcal's `api.calendar.*`. A
|
|
19
|
+
* key with `when` is checked only for some arguments, such as a stage that closes a deal: shown, never refused.
|
|
20
|
+
*/
|
|
21
|
+
alsoChecks?: (path: readonly string[]) => readonly (PermissionKey | ConditionalKey)[];
|
|
22
|
+
}
|
|
23
|
+
/** A key a write checks only when its arguments ask for it; `when` says which, in a few words. */
|
|
24
|
+
export interface ConditionalKey {
|
|
25
|
+
key: PermissionKey;
|
|
26
|
+
when: string;
|
|
19
27
|
}
|
|
20
28
|
/** Either way of naming a program's keys: a messenger's per-command mapping, or a declared CLI's own. */
|
|
21
29
|
export type PermissionKeySource = PermissionKeyOf | CommandPermissions;
|
package/dist/cli/preview.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare const preview: (command: Command, settings: Pick<Settings, "permi
|
|
|
13
13
|
also?: {
|
|
14
14
|
key: PermissionKey;
|
|
15
15
|
level: string;
|
|
16
|
+
when?: string;
|
|
16
17
|
}[];
|
|
17
18
|
} | undefined;
|
|
18
19
|
validation: {
|
|
@@ -49,6 +50,7 @@ export declare const previewBody: (command: Command, permission: {
|
|
|
49
50
|
also?: {
|
|
50
51
|
key: PermissionKey;
|
|
51
52
|
level: string;
|
|
53
|
+
when?: string;
|
|
52
54
|
}[];
|
|
53
55
|
} | undefined, messenger: boolean) => {
|
|
54
56
|
preview: boolean;
|
|
@@ -59,6 +61,7 @@ export declare const previewBody: (command: Command, permission: {
|
|
|
59
61
|
also?: {
|
|
60
62
|
key: PermissionKey;
|
|
61
63
|
level: string;
|
|
64
|
+
when?: string;
|
|
62
65
|
}[];
|
|
63
66
|
} | undefined;
|
|
64
67
|
validation: {
|
package/dist/cli/preview.js
CHANGED
|
@@ -23,11 +23,15 @@ export const preview = (command, settings, tool, keys, messenger = true) => {
|
|
|
23
23
|
(!declared && key !== null && key !== undefined && WRITE_KEYS.includes(key));
|
|
24
24
|
if (level === "deny" || (writes && level === "readonly"))
|
|
25
25
|
throw new CliError("permission_error", `this profile does not permit ${path.join(" ")}`, { permission: key });
|
|
26
|
-
const also = (declared?.alsoChecks?.(path) ?? []).map((one) =>
|
|
27
|
-
key: one
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
const also = (declared?.alsoChecks?.(path) ?? []).map((one) => {
|
|
27
|
+
const key = typeof one === "string" ? one : one.key;
|
|
28
|
+
return {
|
|
29
|
+
key,
|
|
30
|
+
level: levelFor(permissions, key, declared?.defaults).level,
|
|
31
|
+
...(typeof one === "string" ? {} : { when: one.when }),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
const stopped = also.find((one) => one.when === undefined && (one.level === "deny" || (writes && one.level === "readonly")));
|
|
31
35
|
if (stopped)
|
|
32
36
|
throw new CliError("permission_error", `this profile does not permit ${stopped.key}`, { permission: stopped.key });
|
|
33
37
|
return previewBody(command, { key: key ?? null, level, ...(also.length ? { also } : {}) }, messenger);
|
package/dist/crm/cli/index.js
CHANGED
|
@@ -6,8 +6,8 @@ import { answerEntity } from "../../cli/answer-entity.js";
|
|
|
6
6
|
import { environmentOf, outputFor, runBudgetOf } from "../../cli/context.js";
|
|
7
7
|
import { honoursDryRun } from "../../cli/dry-run-guard.js";
|
|
8
8
|
import { FETCH, LOCAL_READ, NO_EFFECTS, REMOTE_WRITE } from "../../cli/effects.js";
|
|
9
|
-
import { itemOf, listOf, renderAnswer, snakeKeys } from "../../cli/envelope.js";
|
|
10
|
-
import { queryTerms, termField, withQuery } from "../../cli/list-query.js";
|
|
9
|
+
import { itemOf, listOf, renderAnswer, snakeKeys, snakeKeysKeeping } from "../../cli/envelope.js";
|
|
10
|
+
import { queryTerms, termField, unfilteredQuery, withQuery } from "../../cli/list-query.js";
|
|
11
11
|
import { terminalAsker } from "../../cli/messenger/ask.js";
|
|
12
12
|
import { lineOf } from "../../cli/steps.js";
|
|
13
13
|
import { CRM_KINDS } from "../../domain/crm.js";
|
|
@@ -41,7 +41,15 @@ export const STAGES_QUERY = {
|
|
|
41
41
|
],
|
|
42
42
|
};
|
|
43
43
|
/** A CRM's users typed these; a list item names its own, as `events list` does. */
|
|
44
|
-
const DEAL_UNTRUSTED = [
|
|
44
|
+
const DEAL_UNTRUSTED = [
|
|
45
|
+
"title",
|
|
46
|
+
"summary",
|
|
47
|
+
"stage",
|
|
48
|
+
"pipeline_name",
|
|
49
|
+
"organization.name",
|
|
50
|
+
"people.name",
|
|
51
|
+
"provider_metadata",
|
|
52
|
+
];
|
|
45
53
|
const PIPELINE_UNTRUSTED = ["name", "stages.name"];
|
|
46
54
|
const STAGE_UNTRUSTED = ["name", "pipeline_name"];
|
|
47
55
|
const crmSync = v.strictObject({
|
|
@@ -92,18 +100,36 @@ const crmWrite = v.strictObject({
|
|
|
92
100
|
lead: v.optional(dealEntity),
|
|
93
101
|
untrusted: v.array(text),
|
|
94
102
|
});
|
|
103
|
+
/** b24's `fields list` answer, so a CRM CLI's own command can go; `untrusted` names what the CRM's users typed. */
|
|
104
|
+
const crmField = v.strictObject({
|
|
105
|
+
code: v.string(),
|
|
106
|
+
label: v.nullable(v.string()),
|
|
107
|
+
type: v.string(),
|
|
108
|
+
multiple: v.boolean(),
|
|
109
|
+
filterable: v.boolean(),
|
|
110
|
+
options: v.optional(v.array(v.strictObject({ id: v.string(), label: v.string() }))),
|
|
111
|
+
untrusted: v.array(text),
|
|
112
|
+
});
|
|
113
|
+
const FIELDS_QUERY = unfilteredQuery("crm_field", crmField);
|
|
114
|
+
const FIELD_UNTRUSTED = ["label", "options.label"];
|
|
115
|
+
/** The words `fields list` takes: its sibling commands' nouns. */
|
|
116
|
+
const FIELD_KINDS = {
|
|
117
|
+
deals: "deal",
|
|
118
|
+
leads: "lead",
|
|
119
|
+
people: "person",
|
|
120
|
+
organizations: "organization",
|
|
121
|
+
};
|
|
95
122
|
const crmRemoval = v.strictObject({ operation_id: text, ref: text, removed: count });
|
|
96
123
|
const collect = (value, previous = []) => {
|
|
97
124
|
if (value.indexOf("=") < 1)
|
|
98
|
-
throw new InvalidArgumentError("expected
|
|
125
|
+
throw new InvalidArgumentError("expected key=value");
|
|
99
126
|
return [...previous, value];
|
|
100
127
|
};
|
|
101
|
-
/** `--field
|
|
102
|
-
const
|
|
128
|
+
/** `--field` as typed, split at the first `=`. */
|
|
129
|
+
const pairsOf = (typed) => typed.map((pair) => {
|
|
103
130
|
const at = pair.indexOf("=");
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}));
|
|
131
|
+
return [pair.slice(0, at), pair.slice(at + 1)];
|
|
132
|
+
});
|
|
107
133
|
const integer = (most) => (value) => {
|
|
108
134
|
if (!/^[1-9]\d*$/.test(value) || Number(value) > most)
|
|
109
135
|
throw new InvalidArgumentError(`expected 1 to ${most}`);
|
|
@@ -111,7 +137,9 @@ const integer = (most) => (value) => {
|
|
|
111
137
|
};
|
|
112
138
|
/** The words below the program, as `page.next` and `sync`'s next step run it again. */
|
|
113
139
|
const pathOf = (command) => command.parent?.parent ? [...pathOf(command.parent), command.name()] : [command.name()];
|
|
114
|
-
|
|
140
|
+
/** A CRM's own field codes are its keys, never ours to rename. */
|
|
141
|
+
const dealKeys = snakeKeysKeeping(["providerMetadata"]);
|
|
142
|
+
const marked = (items, untrusted) => items.map((item) => ({ ...dealKeys(item), untrusted }));
|
|
115
143
|
const addCrmCommandsDefinition = (program, options) => {
|
|
116
144
|
const now = () => options.now?.() ?? Date.now();
|
|
117
145
|
const within = async (command, work) => {
|
|
@@ -237,7 +265,7 @@ const addCrmCommandsDefinition = (program, options) => {
|
|
|
237
265
|
.argument("<ref>", `${kind}:<id>, as a list shows it`), `${plural} show ${kind}:12`).action(async (reference, _opts, command) => {
|
|
238
266
|
await within(command, async (run) => {
|
|
239
267
|
const found = await ownCopy(run, kind, reference);
|
|
240
|
-
return itemOf({ ...
|
|
268
|
+
return itemOf({ ...dealKeys(found), untrusted: DEAL_UNTRUSTED });
|
|
241
269
|
});
|
|
242
270
|
});
|
|
243
271
|
addWrites(group, kind);
|
|
@@ -272,7 +300,7 @@ const addCrmCommandsDefinition = (program, options) => {
|
|
|
272
300
|
};
|
|
273
301
|
const answered = (operationId, item) => itemOf({
|
|
274
302
|
operation_id: operationId,
|
|
275
|
-
[kind]: { ...
|
|
303
|
+
[kind]: { ...dealKeys(item), untrusted: DEAL_UNTRUSTED },
|
|
276
304
|
untrusted: DEAL_UNTRUSTED.map((field) => `${kind}.${field}`),
|
|
277
305
|
});
|
|
278
306
|
const write = (name, verb, description) => {
|
|
@@ -292,12 +320,14 @@ const addCrmCommandsDefinition = (program, options) => {
|
|
|
292
320
|
.option("--currency <code>", "an ISO 4217 code, such as EUR")
|
|
293
321
|
.option("--close-date <date>", "the day it is expected to close, YYYY-MM-DD")
|
|
294
322
|
.option("--owner <user>", "the CRM user it is assigned to; anyone but the sign-in's own user checks crm.assign-others")
|
|
295
|
-
.option("--field <key=value>", "one of the CRM's own fields, by its code
|
|
323
|
+
.option("--field <key=value>", "one of the CRM's own fields, by its code or label as `fields list` prints them, and its value; a list " +
|
|
324
|
+
"field takes its option's label or id; repeat it for more fields, or for each value of a multiple field; " +
|
|
325
|
+
"an empty value clears it", collect,
|
|
296
326
|
// The empty default marks the flag as repeatable, so an agent's tool takes a list.
|
|
297
327
|
[]);
|
|
298
328
|
const inputOf = ({ field, operationId: _id, ...rest }) => ({
|
|
299
329
|
...Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined)),
|
|
300
|
-
...(field?.length ? { custom:
|
|
330
|
+
...(field?.length ? { custom: pairsOf(field) } : {}),
|
|
301
331
|
});
|
|
302
332
|
const idOf = (opts) => opts.operationId === undefined ? {} : { operationId: opts.operationId };
|
|
303
333
|
const run = (command, work) => within(command, (opened) => work(contextOf(opened, command)));
|
|
@@ -408,6 +438,33 @@ const addCrmCommandsDefinition = (program, options) => {
|
|
|
408
438
|
});
|
|
409
439
|
});
|
|
410
440
|
});
|
|
441
|
+
const fieldKinds = Object.keys(FIELD_KINDS).filter((word) => word !== "leads" || options.leads !== false);
|
|
442
|
+
const fields = annotate(program.command("fields"), { effects: NO_EFFECTS }).description("the CRM's own fields on deals, leads, people and organizations, as last synced");
|
|
443
|
+
example(withQuery(answerEntity(annotate(fields.command("list"), { effects: LOCAL_READ }), "crm_field", crmField)
|
|
444
|
+
.description("the CRM's custom fields of one kind, in its order, with a list field's options")
|
|
445
|
+
.argument("<kind>", fieldKinds.join(", "))
|
|
446
|
+
.option("--limit <n>", `fields per page, 1–${MOST}`, integer(MOST), 50)
|
|
447
|
+
.option("--page <n>", "which page, starting at 1", integer(1_000_000), 1)
|
|
448
|
+
.option("--all", "every field, no paging"), FIELDS_QUERY), "fields list deals", "fields list people --json").action(async (kind, words, opts, command) => {
|
|
449
|
+
if (!fieldKinds.includes(kind))
|
|
450
|
+
throw new CliError("validation_error", `fields are listed for ${fieldKinds.join(", ")}; got ${kind}`, {
|
|
451
|
+
reason: "field.unknown-kind",
|
|
452
|
+
allowed: fieldKinds,
|
|
453
|
+
});
|
|
454
|
+
queryTerms(words, FIELDS_QUERY, "fields list");
|
|
455
|
+
await within(command, async (run) => {
|
|
456
|
+
const found = await run.store.crm.fields(run.account, FIELD_KINDS[kind]);
|
|
457
|
+
const offset = opts.all ? 0 : (opts.page - 1) * opts.limit;
|
|
458
|
+
const end = opts.all ? found.length : offset + opts.limit;
|
|
459
|
+
return listOf(pathOf(command).join(" "), {
|
|
460
|
+
items: found.slice(offset, end).map((field) => ({ ...field, untrusted: FIELD_UNTRUSTED })),
|
|
461
|
+
limit: opts.all ? Math.max(1, found.length) : opts.limit,
|
|
462
|
+
page: opts.page,
|
|
463
|
+
has_more: found.length > end,
|
|
464
|
+
...(words.length ? { args: { query: words } } : {}),
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
});
|
|
411
468
|
const search = program.commands.find((one) => one.name() === "search") ??
|
|
412
469
|
annotate(program.command("search"), { effects: NO_EFFECTS }).description("search what the local store holds");
|
|
413
470
|
addCrmSearchAll(search, { within, app: options.app, example });
|
package/dist/crm/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { CrmChanges, CrmIdPage, CrmPage, CrmSource, CrmSourceTraits } from "./source.js";
|
|
2
2
|
export { type CrmCursor, type CrmSyncIssue, type CrmSyncOptions, type CrmSyncReport, type CrmSyncResult, crmStatus, cursorName, parseCrmCursor, syncCrm, } from "./sync.js";
|
|
3
|
-
export { CRM_ASSIGN_OTHERS_KEY, CRM_VERBS, CRM_WRITE_DEFAULTS, CRM_WRITE_KEYS, type CrmDraft, type CrmFields, type CrmVerb, type CrmWriteCapabilities, type CrmWriteOptions, type CrmWriter, } from "./writer.js";
|
|
3
|
+
export { CRM_ASSIGN_OTHERS_KEY, CRM_VERBS, CRM_WRITE_DEFAULTS, CRM_WRITE_KEYS, type CrmDraft, type CrmFields, type CrmVerb, type CrmWriteCapabilities, type CrmWriteOptions, type CrmWriter, crmAlsoChecks, } from "./writer.js";
|
|
4
4
|
export { type CrmFieldsInput, type CrmWriteContext, type CrmWrites, type CrmWritten, closeCrmItem, createCrmItem, moveCrmItem, removeCrmItem, updateCrmItem, } from "./writes.js";
|