@wirecat/cli-core 0.544.0 → 0.546.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 +88 -0
- package/dist/calendar/busy.js +4 -1
- package/dist/calendar/cli/answers.d.ts +31 -0
- package/dist/calendar/cli/answers.js +8 -0
- package/dist/calendar/cli/index.d.ts +6 -0
- package/dist/calendar/cli/index.js +43 -4
- package/dist/calendar/cli/remind.js +2 -1
- package/dist/calendar/expand.d.ts +17 -0
- package/dist/calendar/expand.js +340 -0
- package/dist/calendar/feeds.d.ts +96 -0
- package/dist/calendar/feeds.js +324 -0
- package/dist/calendar/ics.d.ts +15 -4
- package/dist/calendar/ics.js +18 -7
- package/dist/calendar/index.d.ts +3 -1
- package/dist/calendar/index.js +2 -0
- package/dist/calendar/server-search.js +3 -1
- package/dist/calendar/writes.js +15 -4
- package/dist/cli/after-dashes.d.ts +16 -0
- package/dist/cli/after-dashes.js +41 -0
- package/dist/cli/bot/context.d.ts +1 -1
- package/dist/cli/bot/context.js +9 -1
- package/dist/cli/bot/messages.js +2 -0
- package/dist/cli/bot/setup.js +1 -0
- package/dist/cli/contract-templates.js +2 -2
- package/dist/cli/entry.d.ts +2 -0
- package/dist/cli/entry.js +3 -2
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/messenger/admin-commands.js +5 -1
- package/dist/cli/messenger/admin-contacts-command.js +5 -1
- package/dist/cli/messenger/admin-folders-command.js +5 -1
- package/dist/cli/messenger/archive-commands.js +20 -2
- package/dist/cli/messenger/backfill-command.js +13 -2
- package/dist/cli/messenger/complete-command.js +1 -0
- package/dist/cli/messenger/context.js +3 -0
- package/dist/cli/messenger/history-gaps.d.ts +12 -0
- package/dist/cli/messenger/history-gaps.js +41 -0
- package/dist/cli/messenger/messages-command.js +9 -22
- package/dist/cli/messenger/messages-edit-command.js +1 -0
- package/dist/cli/messenger/messages-press-command.js +1 -0
- package/dist/cli/messenger/messages-search-command.js +5 -1
- package/dist/cli/messenger/messages-send-command.js +1 -0
- package/dist/cli/messenger/observed.js +2 -2
- package/dist/cli/messenger/polls-command.js +1 -0
- package/dist/cli/messenger/private-people-command.js +5 -1
- package/dist/cli/messenger/search-answers.d.ts +3 -2
- package/dist/cli/messenger/search-answers.js +12 -5
- package/dist/cli/messenger/search-command.js +3 -1
- package/dist/cli/messenger/stored.d.ts +6 -1
- package/dist/cli/messenger/stored.js +94 -5
- package/dist/cli/messenger/topics-command.js +1 -1
- package/dist/cli/program.js +17 -6
- package/dist/cli/schedule/schedule-command.js +1 -0
- package/dist/cli/settings.d.ts +9 -0
- package/dist/cli/settings.js +20 -0
- package/dist/cli/shared-command-exports.d.ts +24 -0
- package/dist/cli/steps.js +1 -0
- package/dist/domain/message-link.d.ts +3 -0
- package/dist/domain/message-link.js +9 -14
- package/dist/domain/money-words.d.ts +11 -0
- package/dist/domain/money-words.js +38 -0
- package/dist/domain/providers.d.ts +2 -0
- package/dist/domain/providers.js +15 -3
- package/dist/domain/ref-arguments.d.ts +31 -0
- package/dist/domain/ref-arguments.js +79 -0
- package/dist/domain/references.d.ts +20 -2
- package/dist/domain/references.js +132 -47
- package/dist/entities/index.d.ts +1 -0
- package/dist/entities/message-transfer.d.ts +4 -0
- package/dist/entities/message-transfer.js +2 -0
- package/dist/errors/reasons.js +18 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/oauth/client-file.d.ts +14 -0
- package/dist/oauth/client-file.js +28 -0
- package/dist/oauth/index.d.ts +1 -0
- package/dist/oauth/index.js +1 -0
- package/dist/ports/messenger.d.ts +3 -2
- package/dist/resolve.d.ts +3 -1
- package/dist/resolve.js +8 -0
- package/dist/search/task-signature.d.ts +5 -1
- package/dist/search/task-signature.js +27 -9
- package/dist/services/agenda.d.ts +5 -0
- package/dist/services/agenda.js +3 -1
- package/dist/services/history-limits.d.ts +45 -0
- package/dist/services/history-limits.js +50 -0
- package/dist/services/index.d.ts +5 -1
- package/dist/services/index.js +5 -1
- package/dist/services/message-resolution.d.ts +4 -1
- package/dist/services/message-resolution.js +19 -3
- package/dist/services/messages.js +15 -13
- package/dist/services/money-words.d.ts +3 -0
- package/dist/services/money-words.js +5 -0
- package/dist/services/questions.d.ts +7 -2
- package/dist/services/questions.js +13 -4
- package/dist/services/stored-project-gathering.d.ts +9 -0
- package/dist/services/stored-project-gathering.js +9 -4
- package/dist/services/task-categories.d.ts +2 -0
- package/dist/services/task-categories.js +19 -8
- package/dist/store/chats.d.ts +10 -0
- package/dist/store/chats.js +25 -0
- package/dist/store/sqlite/chats.js +2 -1
- package/dist/store/sqlite/evidence-sources.js +2 -2
- package/dist/store/sqlite/history-skips.d.ts +24 -0
- package/dist/store/sqlite/history-skips.js +95 -0
- package/dist/store/sqlite/identities.js +1 -1
- package/dist/store/sqlite/knowledge.js +7 -2
- package/dist/store/sqlite/notes.js +4 -4
- package/dist/store/sqlite/things.d.ts +0 -1
- package/dist/store/sqlite/things.js +25 -24
- package/dist/store/store.d.ts +1 -0
- package/package.json +1 -1
- package/parity.json +4 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { CliError, isCliError } from "@wirecat/cli-base";
|
|
3
|
+
import { zonedMoment } from "@wirecat/cli-base/query";
|
|
4
|
+
import { runBudget } from "../background/budget.js";
|
|
5
|
+
import { calendarText, joinUrlWithoutQuery } from "../meetings/index.js";
|
|
6
|
+
import { expandSeries, unexpandable } from "./expand.js";
|
|
7
|
+
import { readIcs } from "./ics.js";
|
|
8
|
+
import { syncCalendars } from "./sync.js";
|
|
9
|
+
/** A feed's calendar id: `ics:` and a random id, never the address or anything made from it. */
|
|
10
|
+
export const FEED_PREFIX = "ics:";
|
|
11
|
+
export const isFeedCalendar = (calendar) => calendar.externalId.startsWith(FEED_PREFIX);
|
|
12
|
+
export const FEED_LIMITS = { maxBytes: 10 * 1024 * 1024, timeoutMs: 30_000, redirects: 5 };
|
|
13
|
+
const invalidAddress = (why) => new CliError("validation_error", `the feed address ${why}`, { reason: "feed.address-invalid" });
|
|
14
|
+
/** `webcal://` is `https://`: calendar apps write it so a click opens them, and the feed itself is served over https. */
|
|
15
|
+
export const feedAddress = (text) => {
|
|
16
|
+
let url;
|
|
17
|
+
try {
|
|
18
|
+
url = new URL(text.trim().replace(/^webcals?:\/\//iu, "https://"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw invalidAddress("is not a URL; give the feed's https:// or webcal:// address");
|
|
22
|
+
}
|
|
23
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
24
|
+
throw invalidAddress("must start with https://, http:// or webcal://");
|
|
25
|
+
if (url.username || url.password)
|
|
26
|
+
throw invalidAddress("must not hold a user name or a password");
|
|
27
|
+
return url;
|
|
28
|
+
};
|
|
29
|
+
const sameAddress = (a, b) => feedAddress(a).href === feedAddress(b).href;
|
|
30
|
+
const readBody = async (response, name, maxBytes) => {
|
|
31
|
+
const reader = response.body?.getReader();
|
|
32
|
+
if (!reader)
|
|
33
|
+
return "";
|
|
34
|
+
const chunks = [];
|
|
35
|
+
let size = 0;
|
|
36
|
+
for (;;) {
|
|
37
|
+
const { done, value } = await reader.read();
|
|
38
|
+
if (done)
|
|
39
|
+
break;
|
|
40
|
+
size += value.byteLength;
|
|
41
|
+
if (size > maxBytes) {
|
|
42
|
+
await reader.cancel();
|
|
43
|
+
throw new CliError("validation_error", `${name} is larger than ${Math.round(maxBytes / 1024 / 1024)} MB`, {
|
|
44
|
+
reason: "feed.too-large",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
chunks.push(value);
|
|
48
|
+
}
|
|
49
|
+
return new TextDecoder("utf-8").decode(Buffer.concat(chunks));
|
|
50
|
+
};
|
|
51
|
+
const STATUS_ERRORS = {
|
|
52
|
+
401: ["authentication_error", "feed.refused"],
|
|
53
|
+
403: ["permission_error", "feed.refused"],
|
|
54
|
+
404: ["not_found", "feed.gone"],
|
|
55
|
+
410: ["not_found", "feed.gone"],
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The feed's text: http or https, each redirect followed by hand and only to https, the body cut off at its limit and
|
|
59
|
+
* the request at its time. Errors name the feed and the status, never the address, which may hold a token.
|
|
60
|
+
*/
|
|
61
|
+
export const fetchFeed = async (address, options) => {
|
|
62
|
+
const { name, limits = FEED_LIMITS } = options;
|
|
63
|
+
const run = options.fetch ?? fetch;
|
|
64
|
+
const timeout = AbortSignal.timeout(limits.timeoutMs);
|
|
65
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
66
|
+
let url = feedAddress(address);
|
|
67
|
+
try {
|
|
68
|
+
for (let hop = 0;; hop++) {
|
|
69
|
+
const response = await run(url, {
|
|
70
|
+
redirect: "manual",
|
|
71
|
+
signal,
|
|
72
|
+
headers: { accept: "text/calendar, text/plain;q=0.5, */*;q=0.1" },
|
|
73
|
+
});
|
|
74
|
+
const location = response.headers.get("location");
|
|
75
|
+
if (response.status >= 300 && response.status < 400 && location) {
|
|
76
|
+
await response.body?.cancel();
|
|
77
|
+
if (hop >= limits.redirects)
|
|
78
|
+
throw new CliError("provider_error", `${name} sent the request on more than ${limits.redirects} times`, {
|
|
79
|
+
reason: "feed.refused",
|
|
80
|
+
});
|
|
81
|
+
const next = new URL(location, url);
|
|
82
|
+
if (next.protocol !== "https:")
|
|
83
|
+
throw new CliError("validation_error", `${name} sent the request on to an address that is not https`, {
|
|
84
|
+
reason: "feed.redirect-refused",
|
|
85
|
+
});
|
|
86
|
+
url = next;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
await response.body?.cancel();
|
|
91
|
+
const [code, reason] = STATUS_ERRORS[response.status] ?? ["provider_error", "feed.refused"];
|
|
92
|
+
throw new CliError(code, `${name} answered HTTP ${response.status}`, {
|
|
93
|
+
reason,
|
|
94
|
+
status: response.status,
|
|
95
|
+
retryable: response.status >= 500 || response.status === 429,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return await readBody(response, name, limits.maxBytes);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (isCliError(error))
|
|
103
|
+
throw error;
|
|
104
|
+
if (options.signal?.aborted)
|
|
105
|
+
throw new CliError("cancelled", `reading ${name} was cancelled`);
|
|
106
|
+
if (timeout.aborted)
|
|
107
|
+
throw new CliError("timeout", `${name} did not answer within ${Math.round(limits.timeoutMs / 1000)} s`, {
|
|
108
|
+
reason: "feed.timeout",
|
|
109
|
+
retryable: true,
|
|
110
|
+
});
|
|
111
|
+
// The cause's code only: a fetch error's message may quote the address.
|
|
112
|
+
const cause = error.cause?.code;
|
|
113
|
+
throw new CliError("network_error", `${name} could not be reached${typeof cause === "string" ? ` (${cause})` : ""}`, {
|
|
114
|
+
reason: "feed.unreachable",
|
|
115
|
+
retryable: true,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const SPAN = { start: 0, end: 0 };
|
|
120
|
+
const firstLink = (text) => text?.match(/https?:\/\/[^\s<>"')\]]+/iu)?.[0];
|
|
121
|
+
/** One feed event in the neutral words, its text cleaned as an invitation's is: feed text is somebody else's. */
|
|
122
|
+
export const feedEntry = (event, zone) => {
|
|
123
|
+
const allDay = event.startDate !== undefined;
|
|
124
|
+
const timezone = allDay ? zone : (event.timezone ?? null);
|
|
125
|
+
const recurrence = event.recurrence?.length ? event.recurrence : null;
|
|
126
|
+
return {
|
|
127
|
+
externalId: event.uid,
|
|
128
|
+
kind: recurrence ? "series" : "single",
|
|
129
|
+
icalUid: event.uid,
|
|
130
|
+
status: event.status ?? "confirmed",
|
|
131
|
+
title: calendarText(event.title, { dropSecretLines: false }),
|
|
132
|
+
description: calendarText(event.description),
|
|
133
|
+
location: calendarText(event.location),
|
|
134
|
+
startsAt: allDay ? zonedMoment(event.startDate, zone, SPAN) : (event.startsAt ?? null),
|
|
135
|
+
endsAt: allDay ? zonedMoment(event.endDate, zone, SPAN) : (event.endsAt ?? null),
|
|
136
|
+
allDay,
|
|
137
|
+
startDate: event.startDate ?? null,
|
|
138
|
+
endDate: event.endDate ?? null,
|
|
139
|
+
timezone,
|
|
140
|
+
recurrence,
|
|
141
|
+
eventType: "default",
|
|
142
|
+
transparency: event.transparency ?? null,
|
|
143
|
+
visibility: null,
|
|
144
|
+
ownResponse: null,
|
|
145
|
+
organizer: event.organizer ? { email: event.organizer.email, name: event.organizer.name ?? null } : null,
|
|
146
|
+
participants: (event.attendees ?? []).map((one) => ({
|
|
147
|
+
email: one.email,
|
|
148
|
+
name: one.name ?? null,
|
|
149
|
+
response: one.response ?? null,
|
|
150
|
+
...(one.optional ? { optional: true } : {}),
|
|
151
|
+
})),
|
|
152
|
+
joinUrl: joinUrlWithoutQuery(firstLink(event.location) ?? firstLink(event.description)),
|
|
153
|
+
conference: null,
|
|
154
|
+
url: event.url ?? null,
|
|
155
|
+
sequence: null,
|
|
156
|
+
// The engine keeps a series' slots as they are while its version stays the same.
|
|
157
|
+
etag: createHash("sha256").update(JSON.stringify(event)).digest("hex").slice(0, 32),
|
|
158
|
+
externalUpdatedAt: null,
|
|
159
|
+
metadata: null,
|
|
160
|
+
attachments: [],
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
const inWindow = (entry, window) => entry.startsAt != null &&
|
|
164
|
+
entry.startsAt < window.to &&
|
|
165
|
+
(entry.kind === "series" || Math.max(entry.endsAt ?? entry.startsAt, entry.startsAt + 1) > window.from);
|
|
166
|
+
const zoneOk = (zone) => {
|
|
167
|
+
if (!zone)
|
|
168
|
+
return false;
|
|
169
|
+
try {
|
|
170
|
+
new Intl.DateTimeFormat("en", { timeZone: zone });
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
/** `X-WR-TIMEZONE`: the zone a feed's floating times are written in, when it names one. */
|
|
178
|
+
const feedZone = (text) => /^X-WR-TIMEZONE:(.+)$/imu.exec(text)?.[1]?.trim();
|
|
179
|
+
/** A feed's events inside the window, its series with their overrides, and what it could not take. */
|
|
180
|
+
export const readFeed = (text, zone, window) => {
|
|
181
|
+
const named = feedZone(text);
|
|
182
|
+
const where = zoneOk(named) ? named : zone;
|
|
183
|
+
const read = readIcs(text, where, { overrides: true });
|
|
184
|
+
let skipped = read.skipped.length;
|
|
185
|
+
const entries = [];
|
|
186
|
+
for (const event of read.events) {
|
|
187
|
+
const entry = feedEntry(event, where);
|
|
188
|
+
if (entry.kind === "series" && unexpandable(entry.recurrence) !== null) {
|
|
189
|
+
skipped++;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!window || inWindow(entry, window))
|
|
193
|
+
entries.push(entry);
|
|
194
|
+
}
|
|
195
|
+
const series = new Set(entries.filter((one) => one.kind === "series").map((one) => one.externalId));
|
|
196
|
+
const inFeed = new Set(read.events.map((one) => one.uid));
|
|
197
|
+
const overrides = new Map();
|
|
198
|
+
for (const override of read.overrides) {
|
|
199
|
+
if (!series.has(override.uid)) {
|
|
200
|
+
if (!inFeed.has(override.uid))
|
|
201
|
+
skipped++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const entry = { ...feedEntry(override, where), originalStartsAt: override.recurrenceId };
|
|
205
|
+
overrides.set(override.uid, [...(overrides.get(override.uid) ?? []), entry]);
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
entries,
|
|
209
|
+
overrides,
|
|
210
|
+
name: calendarText(read.name, { dropSecretLines: false }),
|
|
211
|
+
timezone: zoneOk(named) ? named : null,
|
|
212
|
+
events: read.events.length + read.overrides.length,
|
|
213
|
+
skipped,
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
const calendarOf = (feed, timezone = null) => ({
|
|
217
|
+
externalId: `${FEED_PREFIX}${feed.id}`,
|
|
218
|
+
title: feed.title,
|
|
219
|
+
timezone,
|
|
220
|
+
accessRole: "reader",
|
|
221
|
+
primary: false,
|
|
222
|
+
syncByDefault: true,
|
|
223
|
+
});
|
|
224
|
+
/**
|
|
225
|
+
* The profile's feeds as one calendar source with `expands: "local"` and a full list on every sync: a feed has no
|
|
226
|
+
* cursor. The expander a sync is given reads the overrides of the feed `changes()` just read.
|
|
227
|
+
*/
|
|
228
|
+
export const icsFeeds = (options) => {
|
|
229
|
+
const overridesOf = new WeakMap();
|
|
230
|
+
const stats = new Map();
|
|
231
|
+
const feedOf = async (externalId) => {
|
|
232
|
+
const feed = (await options.registry.read()).find((one) => `${FEED_PREFIX}${one.id}` === externalId);
|
|
233
|
+
if (!feed)
|
|
234
|
+
throw new CliError("configuration_error", `the address of feed ${externalId} is no longer kept on this computer`, {
|
|
235
|
+
reason: "feed.no-address",
|
|
236
|
+
});
|
|
237
|
+
return feed;
|
|
238
|
+
};
|
|
239
|
+
const read = (url, name) => fetchFeed(url, {
|
|
240
|
+
name,
|
|
241
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
242
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
243
|
+
...(options.limits ? { limits: options.limits } : {}),
|
|
244
|
+
});
|
|
245
|
+
const source = {
|
|
246
|
+
provider: "ics",
|
|
247
|
+
traits: { expands: "local", cursorHoldsWindow: false, fullListOnly: true },
|
|
248
|
+
async calendars() {
|
|
249
|
+
return (await options.registry.read()).map((feed) => calendarOf(feed));
|
|
250
|
+
},
|
|
251
|
+
async changes(calendar, { window }) {
|
|
252
|
+
const feed = await feedOf(calendar.externalId);
|
|
253
|
+
const text = await read(feed.url, `feed ${calendar.id}`);
|
|
254
|
+
const found = readFeed(text, calendar.timezone ?? options.zone, window);
|
|
255
|
+
for (const entry of found.entries) {
|
|
256
|
+
const changed = found.overrides.get(entry.externalId);
|
|
257
|
+
if (changed)
|
|
258
|
+
overridesOf.set(entry, changed);
|
|
259
|
+
}
|
|
260
|
+
stats.set(calendar.id, { calendarId: calendar.id, events: found.events, skipped: found.skipped });
|
|
261
|
+
return { entries: found.entries, removed: [], cursor: null };
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
return {
|
|
265
|
+
source,
|
|
266
|
+
expand: (series, window) => expandSeries(series, window, overridesOf.get(series) ?? []),
|
|
267
|
+
async add(url, { title } = {}) {
|
|
268
|
+
const address = feedAddress(url).href;
|
|
269
|
+
const feeds = await options.registry.read();
|
|
270
|
+
if (feeds.some((one) => sameAddress(one.url, address)))
|
|
271
|
+
throw new CliError("validation_error", "this profile follows a feed at this address already", {
|
|
272
|
+
reason: "feed.duplicate",
|
|
273
|
+
});
|
|
274
|
+
const found = readFeed(await read(address, "the feed"), options.zone);
|
|
275
|
+
const feed = {
|
|
276
|
+
id: randomBytes(6).toString("hex"),
|
|
277
|
+
url: address,
|
|
278
|
+
title: title?.trim() || found.name || new URL(address).hostname,
|
|
279
|
+
};
|
|
280
|
+
await options.registry.write([...feeds, feed]);
|
|
281
|
+
return { calendar: calendarOf(feed, found.timezone), events: found.events };
|
|
282
|
+
},
|
|
283
|
+
async remove(externalId) {
|
|
284
|
+
const feeds = await options.registry.read();
|
|
285
|
+
await options.registry.write(feeds.filter((one) => `${FEED_PREFIX}${one.id}` !== externalId));
|
|
286
|
+
},
|
|
287
|
+
stats: () => [...stats.values()],
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* The provider's calendars, then the profile's feeds, under one budget and in one report: the feeds are calendars of
|
|
292
|
+
* the same account, synced by their own source because they expand their series here.
|
|
293
|
+
*/
|
|
294
|
+
export const syncWithFeeds = async (source, feeds, store, options) => {
|
|
295
|
+
const budget = options.budget ?? runBudget();
|
|
296
|
+
const first = await syncCalendars(source, store, { ...options, budget });
|
|
297
|
+
if (!feeds)
|
|
298
|
+
return first;
|
|
299
|
+
const second = await syncCalendars(feeds.source, store, { ...options, budget, expand: feeds.expand });
|
|
300
|
+
if (second.calendars === 0)
|
|
301
|
+
return first;
|
|
302
|
+
const complete = first.complete && second.complete;
|
|
303
|
+
const stopped = first.stopped ?? second.stopped;
|
|
304
|
+
const hint = first.hint ?? second.hint;
|
|
305
|
+
const sum = (field) => first.progress[field] + second.progress[field];
|
|
306
|
+
return {
|
|
307
|
+
...first,
|
|
308
|
+
calendars: first.calendars + second.calendars,
|
|
309
|
+
synced: [...first.synced, ...second.synced],
|
|
310
|
+
notSynced: [...first.notSynced, ...second.notSynced],
|
|
311
|
+
complete,
|
|
312
|
+
state: first.state === "complete" && second.state === "complete" ? "complete" : "partial",
|
|
313
|
+
...(stopped ? { stopped } : {}),
|
|
314
|
+
progress: {
|
|
315
|
+
calendars: sum("calendars"),
|
|
316
|
+
calendarsDone: sum("calendarsDone"),
|
|
317
|
+
requests: budget.used,
|
|
318
|
+
saved: sum("saved"),
|
|
319
|
+
steps: sum("steps"),
|
|
320
|
+
},
|
|
321
|
+
...(hint ? { hint } : {}),
|
|
322
|
+
feeds: feeds.stats(),
|
|
323
|
+
};
|
|
324
|
+
};
|
package/dist/calendar/ics.d.ts
CHANGED
|
@@ -45,11 +45,22 @@ export interface IcsSkip {
|
|
|
45
45
|
uid: string | null;
|
|
46
46
|
reason: string;
|
|
47
47
|
}
|
|
48
|
+
/** One changed occurrence of a repeating event: `recurrenceId` is the slot it replaces, as `originalStartsAt`. */
|
|
49
|
+
export interface IcsOverride extends IcsEvent {
|
|
50
|
+
recurrenceId: number;
|
|
51
|
+
}
|
|
52
|
+
export interface IcsRead {
|
|
53
|
+
events: IcsEvent[];
|
|
54
|
+
skipped: IcsSkip[];
|
|
55
|
+
/** With `overrides`: the changed occurrences, which are otherwise skipped. */
|
|
56
|
+
overrides: IcsOverride[];
|
|
57
|
+
/** The calendar's own name (`X-WR-CALNAME`), as the text has it. */
|
|
58
|
+
name: string | null;
|
|
59
|
+
}
|
|
48
60
|
/**
|
|
49
61
|
* The VEVENTs of an iCalendar text; times with neither `Z` nor a TZID are read in `zone`. An event that cannot be
|
|
50
62
|
* imported alone is in `skipped` with the reason; a text with no VCALENDAR is refused.
|
|
51
63
|
*/
|
|
52
|
-
export declare const readIcs: (text: string, zone: string
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
};
|
|
64
|
+
export declare const readIcs: (text: string, zone: string, options?: {
|
|
65
|
+
overrides?: boolean;
|
|
66
|
+
}) => IcsRead;
|
package/dist/calendar/ics.js
CHANGED
|
@@ -182,12 +182,14 @@ const emailOf = (value) => {
|
|
|
182
182
|
.toLowerCase();
|
|
183
183
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(email) ? email : null;
|
|
184
184
|
};
|
|
185
|
-
const
|
|
185
|
+
const slotMoment = (moment) => ("date" in moment ? Date.parse(`${moment.date}T00:00:00Z`) : moment.at);
|
|
186
|
+
const eventOf = (lines, zone, overrides = false) => {
|
|
186
187
|
const one = (name) => lines.find((line) => line.name === name);
|
|
187
188
|
const uid = one("UID")?.value.trim();
|
|
188
189
|
if (!uid)
|
|
189
190
|
throw new Error("the event has no UID");
|
|
190
|
-
|
|
191
|
+
const recurrenceLine = one("RECURRENCE-ID");
|
|
192
|
+
if (recurrenceLine && !overrides)
|
|
191
193
|
throw new Error("a changed occurrence of a repeating event (RECURRENCE-ID) is not imported on its own");
|
|
192
194
|
const startLine = one("DTSTART");
|
|
193
195
|
if (!startLine)
|
|
@@ -264,13 +266,13 @@ const eventOf = (lines, zone) => {
|
|
|
264
266
|
const url = one("URL")?.value.trim();
|
|
265
267
|
if (url)
|
|
266
268
|
event.url = url;
|
|
267
|
-
return event;
|
|
269
|
+
return recurrenceLine ? { ...event, recurrenceId: slotMoment(momentOf(recurrenceLine, zone)) } : event;
|
|
268
270
|
};
|
|
269
271
|
/**
|
|
270
272
|
* The VEVENTs of an iCalendar text; times with neither `Z` nor a TZID are read in `zone`. An event that cannot be
|
|
271
273
|
* imported alone is in `skipped` with the reason; a text with no VCALENDAR is refused.
|
|
272
274
|
*/
|
|
273
|
-
export const readIcs = (text, zone) => {
|
|
275
|
+
export const readIcs = (text, zone, options = {}) => {
|
|
274
276
|
const unfolded = text.replace(/^\uFEFF/u, "").replace(/\r?\n[ \t]/gu, "");
|
|
275
277
|
const lines = unfolded.split(/\r?\n/u).filter((line) => line.trim() !== "");
|
|
276
278
|
if (!/^BEGIN:VCALENDAR$/iu.test(lines[0]?.trim() ?? ""))
|
|
@@ -279,6 +281,8 @@ export const readIcs = (text, zone) => {
|
|
|
279
281
|
});
|
|
280
282
|
const events = [];
|
|
281
283
|
const skipped = [];
|
|
284
|
+
const overrides = [];
|
|
285
|
+
let name = null;
|
|
282
286
|
let held = null;
|
|
283
287
|
// Components inside a VEVENT, such as VALARM, are passed over whole.
|
|
284
288
|
let inner = 0;
|
|
@@ -291,15 +295,22 @@ export const readIcs = (text, zone) => {
|
|
|
291
295
|
held = [];
|
|
292
296
|
continue;
|
|
293
297
|
}
|
|
294
|
-
if (held === null)
|
|
298
|
+
if (held === null) {
|
|
299
|
+
if (line.name === "X-WR-CALNAME" && name === null)
|
|
300
|
+
name = unescapeText(line.value).trim() || null;
|
|
295
301
|
continue;
|
|
302
|
+
}
|
|
296
303
|
if (line.name === "BEGIN")
|
|
297
304
|
inner++;
|
|
298
305
|
else if (line.name === "END" && inner > 0)
|
|
299
306
|
inner--;
|
|
300
307
|
else if (line.name === "END" && value === "VEVENT") {
|
|
301
308
|
try {
|
|
302
|
-
|
|
309
|
+
const event = eventOf(held, zone, options.overrides);
|
|
310
|
+
if ("recurrenceId" in event)
|
|
311
|
+
overrides.push(event);
|
|
312
|
+
else
|
|
313
|
+
events.push(event);
|
|
303
314
|
}
|
|
304
315
|
catch (error) {
|
|
305
316
|
const uid = held.find(({ name }) => name === "UID")?.value.trim() || null;
|
|
@@ -310,5 +321,5 @@ export const readIcs = (text, zone) => {
|
|
|
310
321
|
else if (inner === 0)
|
|
311
322
|
held.push(line);
|
|
312
323
|
}
|
|
313
|
-
return { events, skipped };
|
|
324
|
+
return { events, skipped, overrides, name };
|
|
314
325
|
};
|
package/dist/calendar/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type { CalendarAccessRole, CalendarInput, EntryAttachment, EntryAttachmentInput, EntryEventType, EntryInput, EntryKind, EntryParticipantInput, EntryRevision, EntryStatus, EntryTransparency, EntryVisibility, EventDetails, EventFilter, EventStore, LinkReview, ParticipantResponse, ReviewDecision, StoredCalendar, StoredEntry, StoredParticipant, } from "../meetings/index.js";
|
|
2
|
-
export {
|
|
2
|
+
export { expandSeries, localMoment, unexpandable } from "./expand.js";
|
|
3
|
+
export { type CalendarFeeds, FEED_LIMITS, FEED_PREFIX, type FeedLimits, type FeedRecord, type FeedRegistry, type FeedSyncStats, type FeedsSyncReport, feedAddress, fetchFeed, icsFeeds, isFeedCalendar, readFeed, syncWithFeeds, } from "./feeds.js";
|
|
4
|
+
export { type IcsEvent, type IcsOverride, type IcsRead, type IcsSkip, readIcs, writeIcs } from "./ics.js";
|
|
3
5
|
export { repeatRule } from "./recurrence.js";
|
|
4
6
|
export type { BusyTimes, CalendarExpander, CalendarSource, CalendarSourceTraits, ChangePage, ChangeResult, FreeBusyRequest, SearchPage, SyncWindow, } from "./source.js";
|
|
5
7
|
export { type CalendarCursor, type CalendarSyncOptions, type CalendarSyncReport, type CalendarSyncResult, calendarStatus, calendarSyncWindow, type PendingRound, parseCalendarCursor, syncCalendars, } from "./sync.js";
|
package/dist/calendar/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { expandSeries, localMoment, unexpandable } from "./expand.js";
|
|
2
|
+
export { FEED_LIMITS, FEED_PREFIX, feedAddress, fetchFeed, icsFeeds, isFeedCalendar, readFeed, syncWithFeeds, } from "./feeds.js";
|
|
1
3
|
export { readIcs, writeIcs } from "./ics.js";
|
|
2
4
|
export { repeatRule } from "./recurrence.js";
|
|
3
5
|
export { calendarStatus, calendarSyncWindow, parseCalendarCursor, syncCalendars, } from "./sync.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { isFeedCalendar } from "./feeds.js";
|
|
2
3
|
const iso = (at) => (at == null ? null : new Date(at).toISOString());
|
|
3
4
|
/**
|
|
4
5
|
* The provider's own search over the calendars the account syncs, earliest first, stored nowhere. A calendar the owner
|
|
@@ -7,7 +8,8 @@ const iso = (at) => (at == null ? null : new Date(at).toISOString());
|
|
|
7
8
|
export const serverSearchAnswer = async (source, store, { accountId, account, query, limit }) => {
|
|
8
9
|
if (!source.search)
|
|
9
10
|
throw new CliError("configuration_error", "this calendar provider has no search of its own");
|
|
10
|
-
|
|
11
|
+
// A feed is read whole into the store, so the stored search already covers it; the provider has never seen it.
|
|
12
|
+
const calendars = (await store.calendars(accountId)).filter((calendar) => calendar.synced && calendar.deletedAt === null && !isFeedCalendar(calendar));
|
|
11
13
|
if (calendars.length === 0)
|
|
12
14
|
throw new CliError("validation_error", "no calendar syncs yet; run sync, or calendars sync <calendar>", {
|
|
13
15
|
reason: "search.no-calendars",
|
package/dist/calendar/writes.js
CHANGED
|
@@ -5,6 +5,7 @@ import { guardFor } from "../sends/guard.js";
|
|
|
5
5
|
import { guardedWrite } from "../sends/guarded.js";
|
|
6
6
|
import { DEFAULT_PERMISSIONS, levelFor } from "../sends/permissions.js";
|
|
7
7
|
import { newOperationId } from "../sends/send-id.js";
|
|
8
|
+
import { isFeedCalendar } from "./feeds.js";
|
|
8
9
|
import { readIcs } from "./ics.js";
|
|
9
10
|
import { calendarIn, entryItem, eventIdOf, eventOf, plusDays } from "./reads.js";
|
|
10
11
|
import { kindBoundLines, repeatRule, ruleCount, ruleEnd, ruleEndingBefore, ruleLine, splitDates } from "./recurrence.js";
|
|
@@ -184,8 +185,17 @@ const savedAfterCreate = async (context, id, save) => {
|
|
|
184
185
|
`sync to store it — do not create it again: a create repeated with ${context.names.id} ${id} finds it`, { ...(isCliError(error) ? error.details : {}), operation_id: id, id });
|
|
185
186
|
}
|
|
186
187
|
};
|
|
188
|
+
const READ_ONLY = new Set(["reader", "free-busy"]);
|
|
189
|
+
/** A calendar the account may only read refuses here, before any question is asked or anything is journaled. */
|
|
190
|
+
const writable = (calendar) => {
|
|
191
|
+
if (READ_ONLY.has(calendar.accessRole ?? ""))
|
|
192
|
+
throw new CliError("permission_error", isFeedCalendar(calendar)
|
|
193
|
+
? `calendar ${calendar.id} is a feed, read again from its address on every sync; its events cannot be changed`
|
|
194
|
+
: `calendar ${calendar.id} is read-only for this account`, { reason: "calendar.read-only", sent: false });
|
|
195
|
+
return calendar;
|
|
196
|
+
};
|
|
187
197
|
export const createEvent = async (context, calendarReference, fields, options = {}) => {
|
|
188
|
-
const calendar = await calendarIn(context.store, context.accountId, calendarReference);
|
|
198
|
+
const calendar = writable(await calendarIn(context.store, context.accountId, calendarReference));
|
|
189
199
|
const draft = draftOf(fields, context.zone, { whole: true });
|
|
190
200
|
const id = idOf(context, options.id);
|
|
191
201
|
const notify = notifyOf(context, options.notify, (draft.attendees?.length ?? 0) > 0);
|
|
@@ -209,7 +219,7 @@ export const createEventFromText = async (context, calendarReference, text, opti
|
|
|
209
219
|
throw new CliError("validation_error", "the text is empty: say what, when and where");
|
|
210
220
|
if (line.length > TEXT_MAX || /[\r\n]/u.test(line))
|
|
211
221
|
throw new CliError("validation_error", `the text is one line of at most ${TEXT_MAX} characters`);
|
|
212
|
-
const calendar = await calendarIn(context.store, context.accountId, calendarReference);
|
|
222
|
+
const calendar = writable(await calendarIn(context.store, context.accountId, calendarReference));
|
|
213
223
|
const notify = options.notify ?? false;
|
|
214
224
|
const guard = writeGuard(context, keysFor(context, "quick-add", notify));
|
|
215
225
|
const id = newEventId();
|
|
@@ -251,7 +261,7 @@ export const importEvents = async (context, calendarReference, text) => {
|
|
|
251
261
|
: "the file holds no event (VEVENT)", { reason: "ics.no-events", skipped: skipped.map(({ uid, reason }) => ({ uid, reason })) });
|
|
252
262
|
if (events.length > IMPORT_MAX)
|
|
253
263
|
throw new CliError("validation_error", `the file holds ${events.length} events; one import takes at most ${IMPORT_MAX}`, { reason: "ics.too-many" });
|
|
254
|
-
const calendar = await calendarIn(context.store, context.accountId, calendarReference);
|
|
264
|
+
const calendar = writable(await calendarIn(context.store, context.accountId, calendarReference));
|
|
255
265
|
const guard = writeGuard(context, keysFor(context, "import", false));
|
|
256
266
|
const id = newEventId();
|
|
257
267
|
const imported = [];
|
|
@@ -300,6 +310,7 @@ const targetOf = async (context, reference, { calendar, scope }) => {
|
|
|
300
310
|
if (live.length > 1)
|
|
301
311
|
throw new CliError("validation_error", `${reference} is on calendars ${live.map(({ calendarId }) => calendarId).join(", ")}; name one with ` +
|
|
302
312
|
`${context.names.calendar}`, { param: context.names.calendar });
|
|
313
|
+
const owner = writable(calendars.find(({ id }) => id === entry.calendarId));
|
|
303
314
|
let target = entry;
|
|
304
315
|
if (scope !== "this") {
|
|
305
316
|
const flag = scope === "series" ? context.names.series : context.names.following;
|
|
@@ -316,7 +327,7 @@ const targetOf = async (context, reference, { calendar, scope }) => {
|
|
|
316
327
|
sent: false,
|
|
317
328
|
});
|
|
318
329
|
return {
|
|
319
|
-
calendar:
|
|
330
|
+
calendar: owner,
|
|
320
331
|
entry,
|
|
321
332
|
target,
|
|
322
333
|
participants: details.participants.filter(({ entryId }) => entryId === entry.id),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
declare module "@wirecat/cli-base/commands" {
|
|
3
|
+
interface CommandMeta {
|
|
4
|
+
/**
|
|
5
|
+
* Every word after `--` stays a positional, even one spelled as this command's option: the command takes free
|
|
6
|
+
* text (a message, a title, another program's arguments), where `--json` may be the text itself.
|
|
7
|
+
*/
|
|
8
|
+
literal_after_dashes?: boolean;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `messages sync -- -100 --json` as `messages sync --json -- -100`: agents copy `-- <id> --json` from old skill
|
|
13
|
+
* text, and a negative id needs no `--` anyway. Only a word that is exactly one of the command's options moves, with
|
|
14
|
+
* the value it takes; a command marked `literal_after_dashes` keeps every word after `--` as it is.
|
|
15
|
+
*/
|
|
16
|
+
export declare const liftOptionsAfterDashes: (argv: readonly string[], target: (words: string[]) => Command | undefined) => string[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { metaOf } from "@wirecat/cli-base/commands";
|
|
2
|
+
const optionsOf = (command) => {
|
|
3
|
+
const found = new Map();
|
|
4
|
+
for (let at = command; at; at = at.parent)
|
|
5
|
+
for (const option of at.options)
|
|
6
|
+
if (option.long && !found.has(option.long))
|
|
7
|
+
found.set(option.long, option);
|
|
8
|
+
return found;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* `messages sync -- -100 --json` as `messages sync --json -- -100`: agents copy `-- <id> --json` from old skill
|
|
12
|
+
* text, and a negative id needs no `--` anyway. Only a word that is exactly one of the command's options moves, with
|
|
13
|
+
* the value it takes; a command marked `literal_after_dashes` keeps every word after `--` as it is.
|
|
14
|
+
*/
|
|
15
|
+
export const liftOptionsAfterDashes = (argv, target) => {
|
|
16
|
+
const end = argv.indexOf("--");
|
|
17
|
+
if (end < 0)
|
|
18
|
+
return [...argv];
|
|
19
|
+
const command = target(argv.slice(0, end));
|
|
20
|
+
if (!command || metaOf(command).literal_after_dashes)
|
|
21
|
+
return [...argv];
|
|
22
|
+
const options = optionsOf(command);
|
|
23
|
+
const moved = [];
|
|
24
|
+
const kept = [];
|
|
25
|
+
for (let index = end + 1; index < argv.length; index++) {
|
|
26
|
+
const word = argv[index];
|
|
27
|
+
const equals = word.indexOf("=");
|
|
28
|
+
const option = word.startsWith("--") ? options.get(equals < 0 ? word : word.slice(0, equals)) : undefined;
|
|
29
|
+
if (!option) {
|
|
30
|
+
kept.push(word);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
moved.push(word);
|
|
34
|
+
// An optional value is left alone: after `--` the next word is as likely the positional as the value.
|
|
35
|
+
if (option.required && equals < 0 && index + 1 < argv.length)
|
|
36
|
+
moved.push(argv[++index]);
|
|
37
|
+
}
|
|
38
|
+
if (moved.length === 0)
|
|
39
|
+
return [...argv];
|
|
40
|
+
return [...argv.slice(0, end), ...moved, "--", ...kept];
|
|
41
|
+
};
|
|
@@ -44,7 +44,7 @@ export declare const botContext: (command: Command, bot: BotMessenger) => {
|
|
|
44
44
|
* its section sets one.
|
|
45
45
|
*/
|
|
46
46
|
guard: (permissionDefaults?: Readonly<Record<PermissionKey, Level>>) => import("../../sends/guard.js").SendGuard;
|
|
47
|
-
/** A chat id, `user:<id>`, or the title of a chat this bot has seen — never a guess. */
|
|
47
|
+
/** A chat id, `user:<id>`, a `chat:` or `msg:` reference, or the title of a chat this bot has seen — never a guess. */
|
|
48
48
|
chatRef: (typed: string) => BotChatRef;
|
|
49
49
|
readSecret: (prompt: string) => Promise<string>;
|
|
50
50
|
/** A client for this token, closed when the command ends. */
|
package/dist/cli/bot/context.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { chatReferenceOf, wrongAccount } from "../../domain/ref-arguments.js";
|
|
2
3
|
import { pickChat } from "../../resolve.js";
|
|
3
4
|
import { sendGuard } from "../../sends/guard.js";
|
|
4
5
|
import { SendJournal } from "../../sends/journal.js";
|
|
@@ -75,9 +76,16 @@ export const botContext = (command, bot) => {
|
|
|
75
76
|
recipients: recipients(),
|
|
76
77
|
warn: base.renderer.warn,
|
|
77
78
|
}),
|
|
78
|
-
/** A chat id, `user:<id>`, or the title of a chat this bot has seen — never a guess. */
|
|
79
|
+
/** A chat id, `user:<id>`, a `chat:` or `msg:` reference, or the title of a chat this bot has seen — never a guess. */
|
|
79
80
|
chatRef: (typed) => {
|
|
80
81
|
const trimmed = typed.trim();
|
|
82
|
+
const named = chatReferenceOf(trimmed, bot.provider);
|
|
83
|
+
if (named) {
|
|
84
|
+
const own = registry.botId();
|
|
85
|
+
if (own === undefined || own !== named.account)
|
|
86
|
+
throw wrongAccount(named, { account: own, profile });
|
|
87
|
+
return named.chat;
|
|
88
|
+
}
|
|
81
89
|
if (/^-?\d+$/.test(trimmed) || /^user:\d+$/.test(trimmed))
|
|
82
90
|
return trimmed;
|
|
83
91
|
const known = registry.list();
|
package/dist/cli/bot/messages.js
CHANGED
|
@@ -63,6 +63,7 @@ const seen = (context, message) => {
|
|
|
63
63
|
context.registry.observe([{ id: message.chatId }]);
|
|
64
64
|
};
|
|
65
65
|
const sendCommand = (bot) => annotate(new Command("send"), {
|
|
66
|
+
literal_after_dashes: true,
|
|
66
67
|
effects: REMOTE_WRITE,
|
|
67
68
|
examples: botExamples(bot, 'messages send Team "Doors open at 9"', 'messages send Team "Here it is" --file report.pdf'),
|
|
68
69
|
})
|
|
@@ -199,6 +200,7 @@ const showCommand = (bot) => annotate(new Command("show"), { effects: REMOTE_REA
|
|
|
199
200
|
});
|
|
200
201
|
});
|
|
201
202
|
const editCommand = (bot) => annotate(new Command("edit"), {
|
|
203
|
+
literal_after_dashes: true,
|
|
202
204
|
effects: REMOTE_WRITE,
|
|
203
205
|
examples: botExamples(bot, 'messages edit Team 101 "Doors open at 10"'),
|
|
204
206
|
})
|
package/dist/cli/bot/setup.js
CHANGED
|
@@ -65,11 +65,11 @@ export const sharedCommandTemplate = (app, name) => {
|
|
|
65
65
|
annotate(notes.command("show"), { effects: LOCAL_READ, output: { entity: "note" } })
|
|
66
66
|
.description("show one note")
|
|
67
67
|
.argument("<note>", "stored note reference");
|
|
68
|
-
annotate(notes.command("add"), { effects: LOCAL_WRITE, output: { entity: "note" } })
|
|
68
|
+
annotate(notes.command("add"), { effects: LOCAL_WRITE, output: { entity: "note" }, literal_after_dashes: true })
|
|
69
69
|
.description("write a note")
|
|
70
70
|
.argument("<text>", "note text")
|
|
71
71
|
.option("--subject <ref>", "stored resource the note describes");
|
|
72
|
-
annotate(notes.command("edit"), { effects: LOCAL_WRITE, output: { entity: "note" } })
|
|
72
|
+
annotate(notes.command("edit"), { effects: LOCAL_WRITE, output: { entity: "note" }, literal_after_dashes: true })
|
|
73
73
|
.description("change a note")
|
|
74
74
|
.argument("<note>", "stored note reference")
|
|
75
75
|
.argument("<text>", "replacement text");
|