@wirecat/cli-core 0.596.0 → 0.598.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 +79 -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/cli/index.js +4 -11
- package/dist/calendar/mcp/index.d.ts +12 -12
- package/dist/cli/context.d.ts +12 -0
- package/dist/cli/context.js +17 -3
- package/dist/cli/contract-templates.js +1 -0
- package/dist/cli/early-refusal.js +7 -5
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +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/cli/program.js +3 -0
- package/dist/crm/cli/index.js +76 -17
- 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/kit/index.d.ts +1 -0
- package/dist/kit/index.js +1 -0
- package/dist/kit/run-scope.d.ts +34 -0
- package/dist/kit/run-scope.js +112 -0
- package/dist/mcp/app-server.js +7 -2
- package/dist/meetings/cli/index.d.ts +5 -0
- package/dist/meetings/cli/index.js +4 -3
- 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,85 @@ 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.598.0 — 07.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`runScopeCases`** (`@wirecat/cli-core/testing`): cases that each remote command of a program runs inside
|
|
15
|
+
cli-core's run, so `--record` keeps a run named after it, and that `--timeout` ends it when the service never
|
|
16
|
+
answers. A command beside the run answers under the process run and fails. `neverAnswers` is the `fetch` for the
|
|
17
|
+
silent run.
|
|
18
|
+
- **`inCommandRun(command, body)`** (`@wirecat/cli-core/cli`): runs `body` inside cli-core's run with the settings of
|
|
19
|
+
the app `run()` was given, which `run()` now hands every command (`environment.resolveSettings`).
|
|
20
|
+
|
|
21
|
+
### Changed — may break callers
|
|
22
|
+
|
|
23
|
+
- **`addMeetingCommands` takes `inRun`**, which `meetings sync` runs inside: pass `inCommandRun` from
|
|
24
|
+
`@wirecat/cli-core/cli`. The meetings module cannot reach cli-core's run itself.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **The shared CRM, calendar and meetings commands run inside cli-core's run.** `sync` and the deal and lead writes
|
|
29
|
+
from `addCrmCommands`, every command from `addCalendarCommands`, and `meetings sync` answered under the process
|
|
30
|
+
run: `--record` kept no run, and `runs show` could not find the id they answered. A CLI that wrapped them in a run
|
|
31
|
+
of its own can drop that.
|
|
32
|
+
- **An MCP server's `--permission` levels refuse a messenger write before it connects.** The early refusal of a change
|
|
33
|
+
on the messenger read the command's flags only, so `mcp serve --permission messages.send=deny` let the command
|
|
34
|
+
connect and left the refusal to the send guard. It now merges the server's levels as `baseContext` does.
|
|
35
|
+
`withAgentLevels(command, flags)` (`@wirecat/cli-core/cli`) is that merge, for a CLI's own resolver.
|
|
36
|
+
- **An app's `mcp serve` over stdio ends when it was stopped before it listened.** `serveAppOverStdio` waited only
|
|
37
|
+
for an abort event, which a signal that had already fired never sends, so the server hung with stdin open.
|
|
38
|
+
|
|
39
|
+
## 0.597.0 — 07.11.2026
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **`--field` by label.** CRM `create` and `update` resolve `--field <key>=<value>` by the synced field definitions:
|
|
44
|
+
a field by its code or whole label (a label two fields share is refused as `field.ambiguous` with both codes, an
|
|
45
|
+
unknown one as `field.unknown` with close candidates), a list field's option by its id or label
|
|
46
|
+
(`field.unknown-option`), and a multiple field's repeats into a list; a single field given twice is refused. A CRM
|
|
47
|
+
whose definitions are not stored takes codes as typed.
|
|
48
|
+
- **`CrmWrites.alsoChecks(kind, verb)`**: a CLI's own keys a write also checks, such as `api.crm.deal.update`.
|
|
49
|
+
`crmAlsoChecks(also)` gives `commandPermissions({ alsoChecks })` every key a CRM write may check, so `--dry-run`
|
|
50
|
+
names the implied move, close and `crm.assign-others` keys and the CLI's own. A key an argument implies is a
|
|
51
|
+
`ConditionalKey` `{ key, when }`: `commandPermissions({ alsoChecks })` may return one, and the preview shows its level
|
|
52
|
+
and `when` but never refuses on it.
|
|
53
|
+
- **A deal's and lead's `provider_metadata` and `assignee`.** The CRM's own field values, by code as
|
|
54
|
+
`{ label, value, option_id? }` (`CrmValue`), kept as stored in `metadata`, codes never renamed; `CrmItem.assignee`, the
|
|
55
|
+
CRM user it is assigned to, answered as the identity `<account>/user/<id>`.
|
|
56
|
+
- **CRM field definitions (store version 54).** `CrmSource.fields?(kind)` lists a CRM's own fields on deals, leads,
|
|
57
|
+
contacts or companies: `CrmField` `{ code, label, type, multiple, filterable, options? }`. `syncCrm` reads them every
|
|
58
|
+
run, after the pipelines, and `store.crm.saveFields` keeps each kind's whole list in `crm_fields`: one the CRM no
|
|
59
|
+
longer lists is marked deleted, and an empty list marks them all. `store.crm.fields(account, kind)` reads them in
|
|
60
|
+
the CRM's order. Values stay in `metadata`, keyed by code. The migration adds one table; `minCompatible` stays 34.
|
|
61
|
+
- **`fields list <deals|leads|people|organizations>`** in `addCrmCommands`: the synced fields of one kind, with
|
|
62
|
+
`--limit`, `--page` and `--all`, in bitrix24-cli's `fields list` shape plus `untrusted`. An unknown kind is refused
|
|
63
|
+
as `field.unknown-kind` with the allowed words.
|
|
64
|
+
- **`crmSourceContract` checks `fields()`** when an adapter has it, and `crmSyncContract` checks they are stored. The
|
|
65
|
+
fake CRM has `setFields`, `calls.fields` and `SOURCE_FIELD` on its deals.
|
|
66
|
+
- **A schedule's Telegram delivery is Telegram HTML.** `--to telegram-bot:<profile>/<chat>` converts the report's
|
|
67
|
+
Markdown (headings, bold, italic, code, links, lists, tables as label lines, rules) to the tags `tg` accepts and
|
|
68
|
+
sends it with `--html`; the 4096 and 1024 character limits count the converted text, and a long report's caption
|
|
69
|
+
is cut until its HTML fits. When `tg` answers `validation_error`, the send is repeated once as plain text and
|
|
70
|
+
recorded as `delivery.html-fallback`; any other failure is never resent. Email stays plain text.
|
|
71
|
+
|
|
72
|
+
### Changed — may break callers
|
|
73
|
+
|
|
74
|
+
- **`CrmFieldsInput.custom` is the `--field` pairs as typed** (`[name, value][]`), and `CrmFields.custom` values may be
|
|
75
|
+
a list for a multiple field.
|
|
76
|
+
- **A write the CRM made whose item could not be read back is journaled as sent.** A `CrmWriter` throws it with
|
|
77
|
+
`sent: true` in the details; the error is answered with the write's `operation_id`, no longer journaled as failed.
|
|
78
|
+
- **A CRM sync with `fields()` makes one more request per kind**, so a `--max-pages` budget reaches the items later.
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
|
|
82
|
+
- **A search over a read-only store no longer fails with "attempt to write a readonly database".** The message index
|
|
83
|
+
top-up and the notes, mail, events, memories and tasks index drains are skipped on a connection with `query_only`
|
|
84
|
+
(every read-only open), so `search all` in a CRM CLI answers from every source of a store only a CRM sync wrote. They
|
|
85
|
+
run on the next writable open.
|
|
86
|
+
|
|
8
87
|
## 0.596.0 — 06.11.2026
|
|
9
88
|
|
|
10
89
|
### 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
|
+
};
|
|
@@ -3,7 +3,7 @@ import { annotate, metaOf, sharedCommand } from "@wirecat/cli-base/commands";
|
|
|
3
3
|
import { timezoneOf } from "@wirecat/cli-base/query";
|
|
4
4
|
import { clockMeta } from "@wirecat/cli-base/standards";
|
|
5
5
|
import { Argument, InvalidArgumentError, Option } from "commander";
|
|
6
|
-
import { environmentOf, outputFor, runBudgetOf } from "../../cli/context.js";
|
|
6
|
+
import { environmentOf, inCommandRun, outputFor, runBudgetOf } from "../../cli/context.js";
|
|
7
7
|
import { honoursDryRun } from "../../cli/dry-run-guard.js";
|
|
8
8
|
import { changesStore, ownerOnlyOption, writesFileOnlyWith } from "../../cli/effects.js";
|
|
9
9
|
import { renderAnswer } from "../../cli/envelope.js";
|
|
@@ -77,17 +77,10 @@ const addCalendarCommandsDefinition = (program, options) => {
|
|
|
77
77
|
renderAnswer(command, output, body, view ?? ((shown) => localTimesView(shown, zone(), output.color)));
|
|
78
78
|
};
|
|
79
79
|
/** Opens the profile for `work` and closes it before the answer prints, also when `work` fails. */
|
|
80
|
-
const within =
|
|
80
|
+
const within = (command, work, view) => inCommandRun(command, async () => {
|
|
81
81
|
const run = await options.open(command, { readOnly: readsOnly(command) });
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
body = await work(run);
|
|
85
|
-
}
|
|
86
|
-
finally {
|
|
87
|
-
await run.close?.();
|
|
88
|
-
}
|
|
89
|
-
answer(command, body, view);
|
|
90
|
-
};
|
|
82
|
+
answer(command, await work(run).finally(() => run.close?.()), view);
|
|
83
|
+
});
|
|
91
84
|
const example = (command, ...lines) => annotate(command, { examples: lines.map((line) => `${options.app.command} personal ${line}`) });
|
|
92
85
|
const calendars = annotate(program.command("calendars").description("the account's calendars"), { effects: GROUP });
|
|
93
86
|
withQuery(example(annotate(calendars.command("list"), { effects: FETCH, offline: false }).description("list the account's calendars from the provider, and which of them sync"), "calendars list --json", "calendars list synced:true"), CALENDARS_QUERY).action(async (words, _opts, command) => {
|
|
@@ -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/context.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface BaseEnvironment {
|
|
|
34
34
|
answer?: (question: string) => Promise<string | null> | string | null;
|
|
35
35
|
/** Set by `run()`; a command's run is recorded only when it is known whose run it is. */
|
|
36
36
|
app?: AppIdentity;
|
|
37
|
+
/** Set by `run()`: the app's own, so a shared command group opens its run with the app's settings. */
|
|
38
|
+
resolveSettings?: (flags: GlobalFlags, options?: ResolveOptions) => Settings;
|
|
37
39
|
/** Set by `run()` from `process.platform` unless a test hands one in: the OS a command's paths and units are for. */
|
|
38
40
|
platform?: NodeJS.Platform;
|
|
39
41
|
/** The local speech recognizer in place of the downloaded model; tests hand one in. */
|
|
@@ -120,6 +122,10 @@ export declare const runBudgetOf: (command: Command, requests?: number) => RunBu
|
|
|
120
122
|
export declare const setExitCode: (command: Command, code: number) => void;
|
|
121
123
|
export declare const provide: (program: Command, environment: BaseEnvironment) => void;
|
|
122
124
|
export declare const environmentOf: <E extends BaseEnvironment = BaseEnvironment>(command: Command) => E;
|
|
125
|
+
/** The command's flags with an MCP server's `--permission` levels first: they reach a run on its agent, never in argv. */
|
|
126
|
+
export declare const withAgentLevels: <F extends {
|
|
127
|
+
permission?: string[];
|
|
128
|
+
}>(command: Command, flags: F) => F;
|
|
123
129
|
/**
|
|
124
130
|
* The input and output `readSecret` uses for this command: the run's own, so a prompt goes where the run's other
|
|
125
131
|
* output goes and a test that captures it sees it. `readSecret(prompt, { ...secretInputOf(this), echo: true })`.
|
|
@@ -166,4 +172,10 @@ export interface BaseContext {
|
|
|
166
172
|
type Resolve = (flags: GlobalFlags, options?: ResolveOptions) => Settings;
|
|
167
173
|
/** Everything a command needs that no messenger decides, resolved once. */
|
|
168
174
|
export declare const baseContext: (command: Command, resolveSettings: Resolve) => BaseContext;
|
|
175
|
+
/**
|
|
176
|
+
* A shared group's command inside cli-core's run, with the settings of the app `run()` was given: recorded, and ended
|
|
177
|
+
* by `--timeout` (WC-LIFE-5, WC-LIFE-15). The group's own opener still decides whether it needs a profile. A program
|
|
178
|
+
* parsed without `run()` has no run to open, so `body` just runs.
|
|
179
|
+
*/
|
|
180
|
+
export declare const inCommandRun: <T>(command: Command, body: (events: EventSink) => Promise<T>) => Promise<T>;
|
|
169
181
|
export {};
|
package/dist/cli/context.js
CHANGED
|
@@ -37,6 +37,11 @@ export const provide = (program, environment) => {
|
|
|
37
37
|
environments.set(program, environment);
|
|
38
38
|
};
|
|
39
39
|
export const environmentOf = (command) => (environments.get(rootOf(command)) ?? {});
|
|
40
|
+
/** The command's flags with an MCP server's `--permission` levels first: they reach a run on its agent, never in argv. */
|
|
41
|
+
export const withAgentLevels = (command, flags) => {
|
|
42
|
+
const overrides = environmentOf(command).agent?.overrides ?? [];
|
|
43
|
+
return overrides.length > 0 ? { ...flags, permission: [...overrides, ...(flags.permission ?? [])] } : flags;
|
|
44
|
+
};
|
|
40
45
|
/**
|
|
41
46
|
* The input and output `readSecret` uses for this command: the run's own, so a prompt goes where the run's other
|
|
42
47
|
* output goes and a test that captures it sees it. `readSecret(prompt, { ...secretInputOf(this), echo: true })`.
|
|
@@ -70,9 +75,7 @@ export const outputFor = (command) => {
|
|
|
70
75
|
export const baseContext = (command, resolveSettings) => {
|
|
71
76
|
const environment = environmentOf(command);
|
|
72
77
|
const env = environment.env ?? process.env;
|
|
73
|
-
const
|
|
74
|
-
const overrides = environment.agent?.overrides ?? [];
|
|
75
|
-
const settings = resolveSettings(overrides.length > 0 ? { ...flags, permission: [...overrides, ...(flags.permission ?? [])] } : flags, { env });
|
|
78
|
+
const settings = resolveSettings(withAgentLevels(command, command.optsWithGlobals()), { env });
|
|
76
79
|
const { renderer, format, color, streams, compact, full } = resolveOutput({
|
|
77
80
|
...settings,
|
|
78
81
|
env,
|
|
@@ -141,6 +144,17 @@ export const baseContext = (command, resolveSettings) => {
|
|
|
141
144
|
},
|
|
142
145
|
};
|
|
143
146
|
};
|
|
147
|
+
/**
|
|
148
|
+
* A shared group's command inside cli-core's run, with the settings of the app `run()` was given: recorded, and ended
|
|
149
|
+
* by `--timeout` (WC-LIFE-5, WC-LIFE-15). The group's own opener still decides whether it needs a profile. A program
|
|
150
|
+
* parsed without `run()` has no run to open, so `body` just runs.
|
|
151
|
+
*/
|
|
152
|
+
export const inCommandRun = (command, body) => {
|
|
153
|
+
const { resolveSettings } = environmentOf(command);
|
|
154
|
+
if (!resolveSettings)
|
|
155
|
+
return body(() => { });
|
|
156
|
+
return baseContext(command, (flags, options) => resolveSettings(flags, { ...options, needsProfile: false })).run(body);
|
|
157
|
+
};
|
|
144
158
|
/** `messages send` — the words that name commands, never the arguments: those can be a message. */
|
|
145
159
|
const commandPath = (command) => {
|
|
146
160
|
const words = [];
|
|
@@ -7,7 +7,7 @@ import { SendJournal, sendsPathFor } from "../sends/journal.js";
|
|
|
7
7
|
import { CEILING_KEY, keyForCommand, levelFor, writeOfKey } from "../sends/permissions.js";
|
|
8
8
|
import { newOperationId } from "../sends/send-id.js";
|
|
9
9
|
import { commandPathOf } from "./command-contract.js";
|
|
10
|
-
import {
|
|
10
|
+
import { withAgentLevels } from "./context.js";
|
|
11
11
|
import { changesMessenger } from "./effects.js";
|
|
12
12
|
import { refuseUnknownPermissionKeys } from "./permission-keys.js";
|
|
13
13
|
import { NONE_NAMED, settingsFor } from "./settings.js";
|
|
@@ -33,7 +33,7 @@ export const refuseWriteBeforeConnecting = (action, { app, env, dryRun, resolve
|
|
|
33
33
|
const key = keyForCommand(path);
|
|
34
34
|
if (!key)
|
|
35
35
|
return;
|
|
36
|
-
const settings = (resolve ?? settingsFor(app).resolveSettings)(action.optsWithGlobals(), {
|
|
36
|
+
const settings = (resolve ?? settingsFor(app).resolveSettings)(withAgentLevels(action, action.optsWithGlobals()), {
|
|
37
37
|
env,
|
|
38
38
|
needsProfile: false,
|
|
39
39
|
readOnly: true,
|
|
@@ -91,9 +91,11 @@ export const refuseByCommandPermissions = (action, permissions, { app, env, dryR
|
|
|
91
91
|
const preview = dryRun && app.messenger === false;
|
|
92
92
|
if (lock && effects?.remote === "write" && !preview)
|
|
93
93
|
throw readOnlyRefusal(lock, `run ${commandPathOf(action).join(" ")}`);
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
const settings = resolve(withAgentLevels(action, action.optsWithGlobals()), {
|
|
95
|
+
env,
|
|
96
|
+
needsProfile: false,
|
|
97
|
+
readOnly: true,
|
|
98
|
+
});
|
|
97
99
|
refuseUnknownPermissionKeys(action, settings, app.command, permissions);
|
|
98
100
|
if (dryRun || settings.sources?.profile === NONE_NAMED)
|
|
99
101
|
return;
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export { type CompactOptions, compactText, entityRows, GENERIC_ROW, type RowColu
|
|
|
66
66
|
export { refuseUnknownKey } from "./config-command.js";
|
|
67
67
|
export { type ConfigIssue, type ConfigIssueKind, checkConfigText, issueMessage, readConfigFile, readConfigText, settingPathOf, withoutSetting, } from "./config-issues.js";
|
|
68
68
|
export { loadSnakeCaseConfig } from "./config-keys.js";
|
|
69
|
-
export { type AgentRun, type BaseContext, type BaseEnvironment, baseContext, environmentOf, outputFor, provide, runBudgetOf, secretInputOf, } from "./context.js";
|
|
69
|
+
export { type AgentRun, type BaseContext, type BaseEnvironment, baseContext, environmentOf, inCommandRun, outputFor, provide, runBudgetOf, secretInputOf, withAgentLevels, } from "./context.js";
|
|
70
70
|
export { addContractInfo, type ContractEvidenceOptions, contractEvidence } from "./contract-evidence.js";
|
|
71
71
|
export { sharedCommandTemplate } from "./contract-templates.js";
|
|
72
72
|
export { type Closeable, withDeadline } from "./deadline.js";
|
|
@@ -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";
|
package/dist/cli/index.js
CHANGED
|
@@ -57,7 +57,7 @@ export { compactText, entityRows, GENERIC_ROW, rowShapeOf, rowsOfEntity, SHARED_
|
|
|
57
57
|
export { refuseUnknownKey } from "./config-command.js";
|
|
58
58
|
export { checkConfigText, issueMessage, readConfigFile, readConfigText, settingPathOf, withoutSetting, } from "./config-issues.js";
|
|
59
59
|
export { loadSnakeCaseConfig } from "./config-keys.js";
|
|
60
|
-
export { baseContext, environmentOf, outputFor, provide, runBudgetOf, secretInputOf, } from "./context.js";
|
|
60
|
+
export { baseContext, environmentOf, inCommandRun, outputFor, provide, runBudgetOf, secretInputOf, withAgentLevels, } from "./context.js";
|
|
61
61
|
export { addContractInfo, contractEvidence } from "./contract-evidence.js";
|
|
62
62
|
export { sharedCommandTemplate } from "./contract-templates.js";
|
|
63
63
|
export { withDeadline } from "./deadline.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;
|