@wirecat/cli-core 0.538.0 → 0.540.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 +49 -0
- package/dist/calendar/busy.js +1 -1
- package/dist/calendar/cli/answers.d.ts +36 -0
- package/dist/calendar/cli/answers.js +17 -0
- package/dist/calendar/cli/index.d.ts +4 -0
- package/dist/calendar/cli/index.js +77 -12
- package/dist/calendar/ics.d.ts +55 -0
- package/dist/calendar/ics.js +314 -0
- package/dist/calendar/index.d.ts +1 -0
- package/dist/calendar/index.js +1 -0
- package/dist/calendar/mcp/index.js +2 -1
- package/dist/calendar/prepare.d.ts +2 -0
- package/dist/calendar/reads.d.ts +17 -4
- package/dist/calendar/reads.js +116 -27
- package/dist/calendar/testing/memory.js +5 -0
- package/dist/calendar/testing/writer.d.ts +1 -1
- package/dist/calendar/testing/writer.js +40 -0
- package/dist/calendar/writer.d.ts +15 -1
- package/dist/calendar/writer.js +2 -0
- package/dist/calendar/writes.d.ts +46 -0
- package/dist/calendar/writes.js +98 -0
- package/dist/cli/bot/mcp.js +2 -2
- package/dist/cli/effects.d.ts +14 -0
- package/dist/cli/effects.js +33 -2
- package/dist/cli/envelope.d.ts +4 -0
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +2 -2
- package/dist/cli/list-query.d.ts +8 -3
- package/dist/cli/list-query.js +21 -15
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/errors/reasons.js +7 -0
- package/dist/mcp/agent.js +12 -3
- package/dist/mcp/app-server.js +2 -2
- package/dist/mcp/server.js +2 -2
- package/dist/meetings/event-store.d.ts +6 -0
- package/dist/meetings/testing/event-store-contract.js +15 -0
- package/dist/store/sqlite/calendar-events.js +15 -0
- package/package.json +3 -3
|
@@ -73,6 +73,46 @@ export const fakeCalendarWriter = (options = {}) => {
|
|
|
73
73
|
of(calendar.externalId).set(id, entry);
|
|
74
74
|
return answer(entry);
|
|
75
75
|
},
|
|
76
|
+
/** Reads no date: the event is the whole line as its title, an hour from noon UTC of 2 November 2026. */
|
|
77
|
+
async quickAdd(calendar, text, { notify }) {
|
|
78
|
+
const id = `quick${++version}`;
|
|
79
|
+
calls.push({ write: "quick-add", calendar: calendar.externalId, externalId: id, notify });
|
|
80
|
+
const noon = Date.UTC(2026, 10, 2, 12);
|
|
81
|
+
const entry = stamp({
|
|
82
|
+
externalId: id,
|
|
83
|
+
icalUid: `${id}@example.com`,
|
|
84
|
+
kind: "single",
|
|
85
|
+
status: "confirmed",
|
|
86
|
+
title: text,
|
|
87
|
+
startsAt: noon,
|
|
88
|
+
endsAt: noon + 3_600_000,
|
|
89
|
+
participants: [],
|
|
90
|
+
});
|
|
91
|
+
of(calendar.externalId).set(id, entry);
|
|
92
|
+
return answer(entry);
|
|
93
|
+
},
|
|
94
|
+
/** Keeps one event per UID on a calendar: a UID sent again replaces the event under the same id. */
|
|
95
|
+
async importEvent(calendar, event) {
|
|
96
|
+
const held = [...of(calendar.externalId).values()].find((one) => one.icalUid === event.uid);
|
|
97
|
+
const id = held?.externalId ?? `imported${++version}`;
|
|
98
|
+
calls.push({ write: "import", calendar: calendar.externalId, externalId: id, notify: false });
|
|
99
|
+
const entry = stamp({
|
|
100
|
+
externalId: id,
|
|
101
|
+
icalUid: event.uid,
|
|
102
|
+
kind: event.recurrence ? "series" : "single",
|
|
103
|
+
status: event.status ?? "confirmed",
|
|
104
|
+
title: event.title,
|
|
105
|
+
description: event.description ?? null,
|
|
106
|
+
location: event.location ?? null,
|
|
107
|
+
...(event.startDate === undefined
|
|
108
|
+
? { startsAt: event.startsAt ?? null, endsAt: event.endsAt ?? null, timezone: event.timezone ?? null }
|
|
109
|
+
: { allDay: true, startDate: event.startDate, endDate: event.endDate ?? null }),
|
|
110
|
+
...(event.recurrence ? { recurrence: event.recurrence } : {}),
|
|
111
|
+
participants: (event.attendees ?? []).map(({ email, name }) => ({ email, name: name ?? null })),
|
|
112
|
+
});
|
|
113
|
+
of(calendar.externalId).set(id, entry);
|
|
114
|
+
return answer(entry);
|
|
115
|
+
},
|
|
76
116
|
async update(calendar, externalId, patch, { scope, etag, notify }) {
|
|
77
117
|
calls.push({ write: "update", calendar: calendar.externalId, externalId, notify, scope });
|
|
78
118
|
const entry = stamp({ ...current(calendar, externalId, etag), ...fields(patch) });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import type { EntryInput, ParticipantResponse, StoredCalendar } from "../meetings/index.js";
|
|
3
3
|
import type { Level, PermissionKey } from "../sends/permissions.js";
|
|
4
|
+
import type { IcsEvent } from "./ics.js";
|
|
4
5
|
/**
|
|
5
6
|
* A new event in neutral words: a timed one has `startsAt` and `endsAt` (epoch ms, end exclusive) in `timezone`; an
|
|
6
7
|
* all-day one has `startDate` and `endDate` (`YYYY-MM-DD`, end exclusive). A repeating one has `recurrence`, RFC 5545
|
|
@@ -64,6 +65,19 @@ export interface CalendarWriter {
|
|
|
64
65
|
etag: string;
|
|
65
66
|
notify: boolean;
|
|
66
67
|
}): Promise<EntryInput>;
|
|
68
|
+
/**
|
|
69
|
+
* Optional: the provider makes an event from one line of text, reading its date, time and place its own way. The
|
|
70
|
+
* provider picks the id, so a retry after `outcome_unknown` may make a second event.
|
|
71
|
+
*/
|
|
72
|
+
quickAdd?(calendar: StoredCalendar, text: string, options: {
|
|
73
|
+
notify: boolean;
|
|
74
|
+
}): Promise<EntryInput>;
|
|
75
|
+
/**
|
|
76
|
+
* Optional: adds a private copy of an event read from an iCalendar file, under its own UID, as Google's
|
|
77
|
+
* `events.import`. The provider mails nobody. Sending the same UID again goes to the provider again; what it does
|
|
78
|
+
* with an event it holds already is its own.
|
|
79
|
+
*/
|
|
80
|
+
importEvent?(calendar: StoredCalendar, event: IcsEvent): Promise<EntryInput>;
|
|
67
81
|
/**
|
|
68
82
|
* Optional: moves the event, keeping its id, to `target`, another calendar of the same account. `etag` is checked
|
|
69
83
|
* before the move; a provider whose move takes no precondition reads the event first. A retry after
|
|
@@ -75,7 +89,7 @@ export interface CalendarWriter {
|
|
|
75
89
|
}): Promise<EntryInput>;
|
|
76
90
|
}
|
|
77
91
|
/** Which write a command makes, for `CalendarWrites.extraKeys`. */
|
|
78
|
-
export type CalendarWrite = "create" | "update" | "delete" | "respond" | "move";
|
|
92
|
+
export type CalendarWrite = "create" | "quick-add" | "import" | "update" | "delete" | "respond" | "move";
|
|
79
93
|
/** Mailing the other attendees: an agent may write to the calendar and still not to people (WC-PERM-14's spirit). */
|
|
80
94
|
export declare const EVENTS_NOTIFY_KEY: PermissionKey;
|
|
81
95
|
export declare const EVENTS_DELETE_SERIES_KEY: PermissionKey;
|
package/dist/calendar/writer.js
CHANGED
|
@@ -10,6 +10,8 @@ export const CALENDAR_WRITE_KEYS = [EVENTS_NOTIFY_KEY, EVENTS_DELETE_SERIES_KEY]
|
|
|
10
10
|
*/
|
|
11
11
|
export const CALENDAR_WRITE_DEFAULTS = {
|
|
12
12
|
"events.create": "allow",
|
|
13
|
+
// One import writes a whole file of events, and may change events the calendar already holds under their UIDs.
|
|
14
|
+
"events.import": "ask",
|
|
13
15
|
"events.update": "allow",
|
|
14
16
|
"events.respond": "allow",
|
|
15
17
|
"events.move": "allow",
|
|
@@ -3,6 +3,7 @@ import type { AppIdentity } from "../ports/app.js";
|
|
|
3
3
|
import type { Warn } from "../runs/events.js";
|
|
4
4
|
import { type Asker, type GuardSettings } from "../sends/guard.js";
|
|
5
5
|
import { type PermissionKey } from "../sends/permissions.js";
|
|
6
|
+
import { entryItem } from "./reads.js";
|
|
6
7
|
import { type CalendarWrite, type CalendarWriter, type EventPatch, type EventResponse } from "./writer.js";
|
|
7
8
|
/** What a CLI or an MCP host hands the calendar writes for one profile. */
|
|
8
9
|
export interface CalendarWrites {
|
|
@@ -83,6 +84,51 @@ export declare const createEvent: (context: WriteContext, calendarReference: str
|
|
|
83
84
|
} | null;
|
|
84
85
|
hint?: string | undefined;
|
|
85
86
|
}>;
|
|
87
|
+
/**
|
|
88
|
+
* An event the provider makes from one line of text, as Google's quickAdd. Nobody is mailed unless `notify` says so:
|
|
89
|
+
* the attendees are known only once the provider has read the text.
|
|
90
|
+
*/
|
|
91
|
+
export declare const createEventFromText: (context: WriteContext, calendarReference: string, text: string, options?: {
|
|
92
|
+
notify?: boolean;
|
|
93
|
+
}) => Promise<{
|
|
94
|
+
operation_id: string;
|
|
95
|
+
event: string | null;
|
|
96
|
+
entry: {
|
|
97
|
+
id: string;
|
|
98
|
+
calendar_id: string;
|
|
99
|
+
external_id: string;
|
|
100
|
+
kind: import("../meetings/event-store.js").EntryKind;
|
|
101
|
+
title: string | null;
|
|
102
|
+
status: import("../meetings/event-store.js").EntryStatus;
|
|
103
|
+
event_type: import("../meetings/event-store.js").EntryEventType | null;
|
|
104
|
+
own_response: import("../meetings/event-store.js").ParticipantResponse | null;
|
|
105
|
+
starts_at: string | null;
|
|
106
|
+
ends_at: string | null;
|
|
107
|
+
join_url: string | null;
|
|
108
|
+
url: string | null;
|
|
109
|
+
deleted_at: string | null;
|
|
110
|
+
} | null;
|
|
111
|
+
hint?: string | undefined;
|
|
112
|
+
}>;
|
|
113
|
+
/** The most events one import sends: a file of more is refused before anything goes. */
|
|
114
|
+
export declare const IMPORT_MAX = 500;
|
|
115
|
+
/**
|
|
116
|
+
* The events of an iCalendar text into a calendar, one provider call each, in the file's order. The whole import is
|
|
117
|
+
* one write for the guard: one question, one journal line. A failure part way names the UIDs already imported.
|
|
118
|
+
*/
|
|
119
|
+
export declare const importEvents: (context: WriteContext, calendarReference: string, text: string) => Promise<{
|
|
120
|
+
operation_id: string;
|
|
121
|
+
calendar: string;
|
|
122
|
+
imported: {
|
|
123
|
+
uid: string;
|
|
124
|
+
event: string | null;
|
|
125
|
+
entry: ReturnType<typeof entryItem> | null;
|
|
126
|
+
}[];
|
|
127
|
+
skipped: {
|
|
128
|
+
uid: string | null;
|
|
129
|
+
reason: string;
|
|
130
|
+
}[];
|
|
131
|
+
}>;
|
|
86
132
|
interface ScopeChoice {
|
|
87
133
|
calendar?: string;
|
|
88
134
|
series?: boolean;
|
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 { readIcs } from "./ics.js";
|
|
8
9
|
import { calendarIn, entryItem, eventIdOf, eventOf, plusDays } from "./reads.js";
|
|
9
10
|
import { kindBoundLines, repeatRule, ruleCount, ruleEnd, ruleEndingBefore, ruleLine, splitDates } from "./recurrence.js";
|
|
10
11
|
import { CALENDAR_WRITE_DEFAULTS, EVENTS_DELETE_SERIES_KEY, EVENTS_NOTIFY_KEY, } from "./writer.js";
|
|
@@ -12,6 +13,8 @@ export const EVENT_RESPONSES = ["accepted", "tentative", "declined"];
|
|
|
12
13
|
/** Each write is checked by its command's path, the key a declared CLI's `commandPermissions` already knows. */
|
|
13
14
|
const COMMAND_KEY = {
|
|
14
15
|
create: "events.create",
|
|
16
|
+
"quick-add": "events.create",
|
|
17
|
+
import: "events.import",
|
|
15
18
|
update: "events.update",
|
|
16
19
|
delete: "events.delete",
|
|
17
20
|
respond: "events.respond",
|
|
@@ -191,6 +194,101 @@ export const createEvent = async (context, calendarReference, fields, options =
|
|
|
191
194
|
const [saved] = await savedAfterCreate(context, id, () => context.store.saveEntries(calendar.id, [entry], context.now));
|
|
192
195
|
return answerOf(id, saved, draft.recurrence !== undefined);
|
|
193
196
|
};
|
|
197
|
+
/** The longest line `createEventFromText` sends; a quick add is one sentence, not a description. */
|
|
198
|
+
const TEXT_MAX = 1000;
|
|
199
|
+
/**
|
|
200
|
+
* An event the provider makes from one line of text, as Google's quickAdd. Nobody is mailed unless `notify` says so:
|
|
201
|
+
* the attendees are known only once the provider has read the text.
|
|
202
|
+
*/
|
|
203
|
+
export const createEventFromText = async (context, calendarReference, text, options = {}) => {
|
|
204
|
+
const quickAdd = context.writes.writer.quickAdd;
|
|
205
|
+
if (!quickAdd)
|
|
206
|
+
throw new CliError("configuration_error", "this provider cannot make an event from text");
|
|
207
|
+
const line = text.trim();
|
|
208
|
+
if (!line)
|
|
209
|
+
throw new CliError("validation_error", "the text is empty: say what, when and where");
|
|
210
|
+
if (line.length > TEXT_MAX || /[\r\n]/u.test(line))
|
|
211
|
+
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);
|
|
213
|
+
const notify = options.notify ?? false;
|
|
214
|
+
const guard = writeGuard(context, keysFor(context, "quick-add", notify));
|
|
215
|
+
const id = newEventId();
|
|
216
|
+
let entry;
|
|
217
|
+
try {
|
|
218
|
+
entry = await guardedWrite(guard, { chatId: null, kind: "edit", key: COMMAND_KEY["quick-add"], operationId: id, sendId: id, notify }, () => quickAdd.call(context.writes.writer, calendar, line, { notify }));
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (isCliError(error) && error.code === "outcome_unknown")
|
|
222
|
+
throw new CliError("outcome_unknown", `${error.message} — the provider may have made the event, and a create from text cannot be retried safely: ` +
|
|
223
|
+
"sync, then look with events list changed_at>=today before adding it again", { ...error.details, operation_id: id, retryable: false });
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
let saved;
|
|
227
|
+
try {
|
|
228
|
+
;
|
|
229
|
+
[saved] = await context.store.saveEntries(calendar.id, [entry], context.now);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
throw new CliError(isCliError(error) ? error.code : "invalid_response", `the provider made event ${entry.externalId}, but the local store did not save it ` +
|
|
233
|
+
`(${error instanceof Error ? error.message : String(error)}); sync to store it — do not create it again`, { ...(isCliError(error) ? error.details : {}), operation_id: id, id: entry.externalId, retryable: false });
|
|
234
|
+
}
|
|
235
|
+
return answerOf(id, saved, (entry.recurrence?.length ?? 0) > 0);
|
|
236
|
+
};
|
|
237
|
+
/** The most events one import sends: a file of more is refused before anything goes. */
|
|
238
|
+
export const IMPORT_MAX = 500;
|
|
239
|
+
/**
|
|
240
|
+
* The events of an iCalendar text into a calendar, one provider call each, in the file's order. The whole import is
|
|
241
|
+
* one write for the guard: one question, one journal line. A failure part way names the UIDs already imported.
|
|
242
|
+
*/
|
|
243
|
+
export const importEvents = async (context, calendarReference, text) => {
|
|
244
|
+
const writer = context.writes.writer;
|
|
245
|
+
if (!writer.importEvent)
|
|
246
|
+
throw new CliError("configuration_error", "this provider cannot import events");
|
|
247
|
+
const { events, skipped } = readIcs(text, context.zone);
|
|
248
|
+
if (events.length === 0)
|
|
249
|
+
throw new CliError("validation_error", skipped.length
|
|
250
|
+
? `no event in the file can be imported: ${skipped.map(({ reason }) => reason).join("; ")}`
|
|
251
|
+
: "the file holds no event (VEVENT)", { reason: "ics.no-events", skipped: skipped.map(({ uid, reason }) => ({ uid, reason })) });
|
|
252
|
+
if (events.length > IMPORT_MAX)
|
|
253
|
+
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);
|
|
255
|
+
const guard = writeGuard(context, keysFor(context, "import", false));
|
|
256
|
+
const id = newEventId();
|
|
257
|
+
const imported = [];
|
|
258
|
+
await guardedWrite(guard, { chatId: null, kind: "edit", key: COMMAND_KEY.import, operationId: id, sendId: id, notify: false }, async () => {
|
|
259
|
+
for (const event of events) {
|
|
260
|
+
try {
|
|
261
|
+
const entry = await writer.importEvent?.call(writer, calendar, event);
|
|
262
|
+
if (!entry)
|
|
263
|
+
throw new CliError("invalid_response", "the provider returned no event");
|
|
264
|
+
const [saved] = await context.store.saveEntries(calendar.id, [entry], context.now);
|
|
265
|
+
imported.push({
|
|
266
|
+
uid: event.uid,
|
|
267
|
+
event: saved?.eventId == null ? null : `event:${saved.eventId}`,
|
|
268
|
+
entry: saved ? entryItem(saved) : null,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
const done = imported.map(({ uid }) => uid);
|
|
273
|
+
throw new CliError(isCliError(error) ? error.code : "provider_error", `${error instanceof Error ? error.message : String(error)} — at the event with UID ${event.uid}; ` +
|
|
274
|
+
`${done.length} of ${events.length} were imported before it (details.imported). Running the import ` +
|
|
275
|
+
"again sends every event again under its UID", {
|
|
276
|
+
...(isCliError(error) ? error.details : {}),
|
|
277
|
+
operation_id: id,
|
|
278
|
+
imported: done,
|
|
279
|
+
failed: event.uid,
|
|
280
|
+
retryable: false,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
return {
|
|
286
|
+
operation_id: id,
|
|
287
|
+
calendar: String(calendar.id),
|
|
288
|
+
imported,
|
|
289
|
+
skipped: skipped.map(({ uid, reason }) => ({ uid, reason })),
|
|
290
|
+
};
|
|
291
|
+
};
|
|
194
292
|
const targetOf = async (context, reference, { calendar, scope }) => {
|
|
195
293
|
const details = await eventOf(context.store, context.accountId, eventIdOf(reference));
|
|
196
294
|
const calendars = await context.store.calendars(context.accountId);
|
package/dist/cli/bot/mcp.js
CHANGED
|
@@ -3,7 +3,7 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
3
3
|
import { annotate, describeProgram } from "@wirecat/cli-base/commands";
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { environmentOf } from "../context.js";
|
|
6
|
-
import { changesAuthorityAt,
|
|
6
|
+
import { changesAuthorityAt, LOCAL_READ, REMOTE_WRITE, readsOverMcpAt } from "../effects.js";
|
|
7
7
|
import { itemOf, renderAnswer } from "../envelope.js";
|
|
8
8
|
import { serverEntry } from "../messenger/mcp-command.js";
|
|
9
9
|
import { nextStep, rootOf } from "../steps.js";
|
|
@@ -37,7 +37,7 @@ export const botMcpCommand = (bot) => {
|
|
|
37
37
|
bot,
|
|
38
38
|
program: { run, commands: describeProgram(rootOf(this)) },
|
|
39
39
|
authority: changesAuthorityAt(rootOf(this)),
|
|
40
|
-
fetchOnly:
|
|
40
|
+
fetchOnly: readsOverMcpAt(rootOf(this)),
|
|
41
41
|
settings: context.settings,
|
|
42
42
|
levels: () => bot.resolveSettings(this.optsWithGlobals(), { env: context.env, kind: "bot" }).permissions,
|
|
43
43
|
env: context.env,
|
package/dist/cli/effects.d.ts
CHANGED
|
@@ -50,6 +50,20 @@ export declare const changesStore: <T extends Command>(command: T) => T;
|
|
|
50
50
|
export declare const fetchesOnly: (command: Command) => boolean;
|
|
51
51
|
/** `fetchesOnly` by command path under `root`, as an MCP catalogue asks it; an unknown path is not one. */
|
|
52
52
|
export declare const fetchesOnlyAt: (root: Command) => (path: readonly string[]) => boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Marks a command whose only local write is the file one owner-only option names (`events export --output`): its
|
|
55
|
+
* effects stay a write, as WC-CMD-17 asks, but a run without the option only reads. An MCP tool never offers the
|
|
56
|
+
* option, so it runs through `read`; `<PREFIX>_READ_ONLY` refuses only a run that gives it. `option` is the option's
|
|
57
|
+
* attribute name, `output`.
|
|
58
|
+
*/
|
|
59
|
+
export declare const writesFileOnlyWith: <T extends Command>(command: T, option: string) => T;
|
|
60
|
+
/**
|
|
61
|
+
* What an MCP server runs through `read`: a command that only fetches into the store (`fetchesOnly`), and one whose
|
|
62
|
+
* only write is a file the tool cannot name (`writesFileOnlyWith`).
|
|
63
|
+
*/
|
|
64
|
+
export declare const readsOverMcp: (command: Command) => boolean;
|
|
65
|
+
/** `readsOverMcp` by command path under `root`, as an MCP catalogue asks it; an unknown path is not one. */
|
|
66
|
+
export declare const readsOverMcpAt: (root: Command) => (path: readonly string[]) => boolean;
|
|
53
67
|
/**
|
|
54
68
|
* Whether `<PREFIX>_READ_ONLY` refuses the command as a change to the store. Every write that stays on this machine
|
|
55
69
|
* is refused unless `destroysNothing` marks it, so a new one fails closed (WC-PERM-12). A fetch (`remote: "read"`)
|
package/dist/cli/effects.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { annotate, annotateOption, metaOf, stepTargets } from "@wirecat/cli-base/commands";
|
|
1
|
+
import { annotate, annotateOption, metaOf, optionMetaOf, stepTargets, } from "@wirecat/cli-base/commands";
|
|
2
2
|
import { Option } from "commander";
|
|
3
3
|
import { readOnlyLockOf, readOnlyRefusal } from "../ports/app.js";
|
|
4
4
|
import { keyForCommand } from "../sends/permissions.js";
|
|
@@ -93,6 +93,36 @@ export const fetchesOnlyAt = (root) => (path) => {
|
|
|
93
93
|
at = at?.commands.find((one) => one.name() === word);
|
|
94
94
|
return at !== undefined && fetchesOnly(at);
|
|
95
95
|
};
|
|
96
|
+
const FILE_OPTION = new WeakMap();
|
|
97
|
+
/**
|
|
98
|
+
* Marks a command whose only local write is the file one owner-only option names (`events export --output`): its
|
|
99
|
+
* effects stay a write, as WC-CMD-17 asks, but a run without the option only reads. An MCP tool never offers the
|
|
100
|
+
* option, so it runs through `read`; `<PREFIX>_READ_ONLY` refuses only a run that gives it. `option` is the option's
|
|
101
|
+
* attribute name, `output`.
|
|
102
|
+
*/
|
|
103
|
+
export const writesFileOnlyWith = (command, option) => {
|
|
104
|
+
const flag = command.options.find((one) => one.attributeName() === option);
|
|
105
|
+
if (!flag || optionMetaOf(flag).mcp !== false)
|
|
106
|
+
throw new Error(`${command.name()}: writesFileOnlyWith needs --${option} added first, as an owner-only option`);
|
|
107
|
+
FILE_OPTION.set(command, option);
|
|
108
|
+
return command;
|
|
109
|
+
};
|
|
110
|
+
const writesFileNow = (command) => {
|
|
111
|
+
const option = FILE_OPTION.get(command);
|
|
112
|
+
return option === undefined || command.opts()[option] !== undefined;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* What an MCP server runs through `read`: a command that only fetches into the store (`fetchesOnly`), and one whose
|
|
116
|
+
* only write is a file the tool cannot name (`writesFileOnlyWith`).
|
|
117
|
+
*/
|
|
118
|
+
export const readsOverMcp = (command) => fetchesOnly(command) || FILE_OPTION.has(command);
|
|
119
|
+
/** `readsOverMcp` by command path under `root`, as an MCP catalogue asks it; an unknown path is not one. */
|
|
120
|
+
export const readsOverMcpAt = (root) => (path) => {
|
|
121
|
+
let at = root;
|
|
122
|
+
for (const word of path)
|
|
123
|
+
at = at?.commands.find((one) => one.name() === word);
|
|
124
|
+
return at !== undefined && readsOverMcp(at);
|
|
125
|
+
};
|
|
96
126
|
/**
|
|
97
127
|
* Whether `<PREFIX>_READ_ONLY` refuses the command as a change to the store. Every write that stays on this machine
|
|
98
128
|
* is refused unless `destroysNothing` marks it, so a new one fails closed (WC-PERM-12). A fetch (`remote: "read"`)
|
|
@@ -103,7 +133,8 @@ export const isStoreChange = (command) => {
|
|
|
103
133
|
return ((effects?.remote === "none" || STORE_WRITES.has(command)) &&
|
|
104
134
|
effects?.local === "write" &&
|
|
105
135
|
!KEPT_UNDER_LOCK.has(command) &&
|
|
106
|
-
!changesAuthority(command)
|
|
136
|
+
!changesAuthority(command) &&
|
|
137
|
+
writesFileNow(command));
|
|
107
138
|
};
|
|
108
139
|
export const STORE_CHANGE_REFUSAL = "change the message store";
|
|
109
140
|
/** ` --dry-run` where the program has it, so an example of a command that calls a provider calls nothing. */
|
package/dist/cli/envelope.d.ts
CHANGED
|
@@ -95,6 +95,8 @@ export declare const answerSchema: (item?: v.GenericSchema) => v.UnionSchema<[v.
|
|
|
95
95
|
}, undefined>, undefined>, undefined>;
|
|
96
96
|
readonly truncated: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
97
97
|
readonly timezone: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
|
|
98
|
+
readonly now: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
99
|
+
readonly now_local: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "an ISO 8601 time with its offset">]>, undefined>;
|
|
98
100
|
readonly window: v.OptionalSchema<v.StrictObjectSchema<{
|
|
99
101
|
readonly since: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
100
102
|
readonly until: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
@@ -148,6 +150,8 @@ export declare const answerSchema: (item?: v.GenericSchema) => v.UnionSchema<[v.
|
|
|
148
150
|
}, undefined>, undefined>, undefined>;
|
|
149
151
|
readonly truncated: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
150
152
|
readonly timezone: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
|
|
153
|
+
readonly now: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
154
|
+
readonly now_local: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "an ISO 8601 time with its offset">]>, undefined>;
|
|
151
155
|
readonly window: v.OptionalSchema<v.StrictObjectSchema<{
|
|
152
156
|
readonly since: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
153
157
|
readonly until: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export { sharedCommandTemplate } from "./contract-templates.js";
|
|
|
66
66
|
export { type Closeable, withDeadline } from "./deadline.js";
|
|
67
67
|
export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
|
|
68
68
|
export { DISCOVERY_DESCRIPTIONS } from "./discovery-descriptions.js";
|
|
69
|
-
export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, fetchesOnly, fetchesOnlyAt, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, previewFlag, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, storeChangeRefusal, } from "./effects.js";
|
|
69
|
+
export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, fetchesOnly, fetchesOnlyAt, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, previewFlag, REMOTE_READ, REMOTE_WRITE, reachesOtherService, readsOverMcp, readsOverMcpAt, refuseUnderReadOnly, storeChangeRefusal, writesFileOnlyWith, } from "./effects.js";
|
|
70
70
|
export { type CliEntry, type EntryContext, type EntryIo, startCli } from "./entry.js";
|
|
71
71
|
export { type AnswerOutput, answerSchema, type Body, type ItemBody, isBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, type ListBody, type ListInput, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
|
|
72
72
|
export { withAbort } from "./execution.js";
|
|
@@ -78,7 +78,7 @@ export { type FirstScreen, type FirstScreenLine, firstScreen, installHelp, WIREC
|
|
|
78
78
|
export { type HelpSection, inSection, SECTIONS } from "./help/sections.js";
|
|
79
79
|
export { bufferedInput, type InputPolicy, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provideInputPolicy, } from "./input-policy.js";
|
|
80
80
|
export { answerFields, checkJson, type EntityFields, entityFields, type JsonFieldsResolver, type JsonRequest, liftJson, } from "./json-fields.js";
|
|
81
|
-
export { commandPath, LIST_QUERY_GUIDE, listQuery, nativeField, nativeTermLists, nativeTerms, pagedList, pageOf, type QueryWords, queryArgument, queryHelp, queryTerms, searchText, termField, timeBounds, typedQuery, unfilteredQuery, withQuery, } from "./list-query.js";
|
|
81
|
+
export { commandPath, LIST_QUERY_GUIDE, listQuery, nativeField, nativeTermLists, nativeTerms, negatableOf, pagedList, pageOf, type QueryWords, queryArgument, queryHelp, queryTerms, searchText, termField, timeBounds, typedQuery, unfilteredQuery, withQuery, } from "./list-query.js";
|
|
82
82
|
export { MCP_HTTP_DEFAULT_PORT, type McpServerOptions, type McpServerTarget, mcpServerCommand, } from "./mcp-server-command.js";
|
|
83
83
|
export { formatMeetingBatch, prepareMeetingBatch } from "./meeting-batch.js";
|
|
84
84
|
export { meetingEvidenceItem, meetingPersonContextItem } from "./meeting-evidence-answer.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -58,7 +58,7 @@ export { sharedCommandTemplate } from "./contract-templates.js";
|
|
|
58
58
|
export { withDeadline } from "./deadline.js";
|
|
59
59
|
export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
|
|
60
60
|
export { DISCOVERY_DESCRIPTIONS } from "./discovery-descriptions.js";
|
|
61
|
-
export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, fetchesOnly, fetchesOnlyAt, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, previewFlag, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, storeChangeRefusal, } from "./effects.js";
|
|
61
|
+
export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, fetchesOnly, fetchesOnlyAt, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, previewFlag, REMOTE_READ, REMOTE_WRITE, reachesOtherService, readsOverMcp, readsOverMcpAt, refuseUnderReadOnly, storeChangeRefusal, writesFileOnlyWith, } from "./effects.js";
|
|
62
62
|
export { startCli } from "./entry.js";
|
|
63
63
|
export { answerSchema, isBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
|
|
64
64
|
export { withAbort } from "./execution.js";
|
|
@@ -70,7 +70,7 @@ export { firstScreen, installHelp, WIRECAT_TOOLS } from "./help/first-screen.js"
|
|
|
70
70
|
export { inSection, SECTIONS } from "./help/sections.js";
|
|
71
71
|
export { bufferedInput, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provideInputPolicy, } from "./input-policy.js";
|
|
72
72
|
export { answerFields, checkJson, entityFields, liftJson, } from "./json-fields.js";
|
|
73
|
-
export { commandPath, LIST_QUERY_GUIDE, listQuery, nativeField, nativeTermLists, nativeTerms, pagedList, pageOf, queryArgument, queryHelp, queryTerms, searchText, termField, timeBounds, typedQuery, unfilteredQuery, withQuery, } from "./list-query.js";
|
|
73
|
+
export { commandPath, LIST_QUERY_GUIDE, listQuery, nativeField, nativeTermLists, nativeTerms, negatableOf, pagedList, pageOf, queryArgument, queryHelp, queryTerms, searchText, termField, timeBounds, typedQuery, unfilteredQuery, withQuery, } from "./list-query.js";
|
|
74
74
|
export { MCP_HTTP_DEFAULT_PORT, mcpServerCommand, } from "./mcp-server-command.js";
|
|
75
75
|
export { formatMeetingBatch, prepareMeetingBatch } from "./meeting-batch.js";
|
|
76
76
|
export { meetingEvidenceItem, meetingPersonContextItem } from "./meeting-evidence-answer.js";
|
package/dist/cli/list-query.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import * as v from "valibot";
|
|
|
4
4
|
import { type ListBody, type ListInput } from "./envelope.js";
|
|
5
5
|
import type { Settings } from "./settings.js";
|
|
6
6
|
export declare const LIST_QUERY_GUIDE = "https://github.com/WireCatLabs/cli-core/blob/main/docs/search/query-language.md";
|
|
7
|
+
/** `[query...]`'s help, which points at the Fields section; an MCP tool has none and says the fields instead. */
|
|
8
|
+
export declare const QUERY_ARGUMENT = "field:value terms that narrow the list \u2014 the fields are below";
|
|
7
9
|
/**
|
|
8
10
|
* A list that narrows by one query: `[query...]` last, a Fields section in its help, and the fields in
|
|
9
11
|
* `commands --json`. The registry is checked here, so a field whose example does not run fails at startup.
|
|
@@ -31,7 +33,9 @@ export declare const pageOf: ({ limit, page, all }: Pick<Settings, "limit" | "pa
|
|
|
31
33
|
/** A page-numbered list's answer; under `--all` its limit is what it holds. */
|
|
32
34
|
export declare const pagedList: <T>(command: string, { limit, page, all }: Pick<Settings, "limit" | "page" | "all">, { items, has_more, ...rest }: Omit<ListInput<T>, "limit" | "page" | "cursor">) => ListBody<T>;
|
|
33
35
|
/** An MCP list's `query` argument: the language the CLI list takes after its arguments. */
|
|
34
|
-
export declare const queryArgument: (registry: QueryRegistry) => v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MaxLengthAction<string, 8192, undefined>, v.DescriptionAction<string,
|
|
36
|
+
export declare const queryArgument: (registry: QueryRegistry) => v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MaxLengthAction<string, 8192, undefined>, v.DescriptionAction<string, string>]>, undefined>;
|
|
37
|
+
/** The fields a native list can also leave out, `-field:value`: what `nativeTermLists` keeps under `-field`. */
|
|
38
|
+
export declare const negatableOf: (registry: QueryRegistry) => string[];
|
|
35
39
|
/**
|
|
36
40
|
* The terms of a query a provider answers: field:value joined by AND, each field once. Anything else is a
|
|
37
41
|
* question the source cannot ask, so it is refused rather than answered from a part of the list.
|
|
@@ -39,9 +43,10 @@ export declare const queryArgument: (registry: QueryRegistry) => v.OptionalSchem
|
|
|
39
43
|
export declare const nativeTerms: (ast: QueryAst | undefined, command: string) => Map<string, Predicate>;
|
|
40
44
|
/**
|
|
41
45
|
* As `nativeTerms`, but each `repeatable` field may be named again, every term kept in order: `person:a person:b`
|
|
42
|
-
* asks for both, as AND reads.
|
|
46
|
+
* asks for both, as AND reads. A `negatable` field may also be excluded, `-field:value` or `NOT field:value`, as
|
|
47
|
+
* often as wanted; those terms are kept under `-field`.
|
|
43
48
|
*/
|
|
44
|
-
export declare const nativeTermLists: (ast: QueryAst | undefined, command: string, repeatable?: readonly string[]) => Map<string, Predicate[]>;
|
|
49
|
+
export declare const nativeTermLists: (ast: QueryAst | undefined, command: string, repeatable?: readonly string[], negatable?: readonly string[]) => Map<string, Predicate[]>;
|
|
45
50
|
/**
|
|
46
51
|
* A store that compares whole values: each field's `field:value` or `field:"two words"`, enum values lowercased
|
|
47
52
|
* as validation already accepts them. A range or wildcard is refused rather than read as a value.
|
package/dist/cli/list-query.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
2
|
-
import { dateRange, defineQuery, fieldsHelp, fieldsNamed, OPERATORS_BY_TYPE, parseLucene, QUERY_LIMITS, queryError, queryFieldsOf, timezoneOf, validateQuery, } from "@wirecat/cli-base/query";
|
|
2
|
+
import { dateRange, defineQuery, fieldsHelp, fieldsNamed, OPERATORS_BY_TYPE, parseLucene, QUERY_LIMITS, queryDescription, queryError, queryFieldsOf, timezoneOf, validateQuery, } from "@wirecat/cli-base/query";
|
|
3
3
|
import * as v from "valibot";
|
|
4
4
|
import { queryWordsAfterOptions } from "../meetings/cli/index.js";
|
|
5
5
|
import { listOf } from "./envelope.js";
|
|
6
6
|
export const LIST_QUERY_GUIDE = "https://github.com/WireCatLabs/cli-core/blob/main/docs/search/query-language.md";
|
|
7
|
+
/** `[query...]`'s help, which points at the Fields section; an MCP tool has none and says the fields instead. */
|
|
8
|
+
export const QUERY_ARGUMENT = "field:value terms that narrow the list — the fields are below";
|
|
7
9
|
/**
|
|
8
10
|
* A list that narrows by one query: `[query...]` last, a Fields section in its help, and the fields in
|
|
9
11
|
* `commands --json`. The registry is checked here, so a field whose example does not run fails at startup.
|
|
@@ -12,9 +14,7 @@ export const withQuery = (command, registry) => {
|
|
|
12
14
|
const query = defineQuery(registry);
|
|
13
15
|
annotate(command, { query, output: { entity: registry.entity } });
|
|
14
16
|
const shown = askable(query);
|
|
15
|
-
queryWordsAfterOptions(command).argument("[query...]", shown.length > 0
|
|
16
|
-
? "field:value terms that narrow the list — the fields are below"
|
|
17
|
-
: "field:value terms; this list filters by none yet");
|
|
17
|
+
queryWordsAfterOptions(command).argument("[query...]", shown.length > 0 ? QUERY_ARGUMENT : "field:value terms; this list filters by none yet");
|
|
18
18
|
if (shown.length > 0)
|
|
19
19
|
command.addHelpText("after", `\n${fieldsHelp({ ...query, fields: shown }, LIST_QUERY_GUIDE)}`);
|
|
20
20
|
return command;
|
|
@@ -97,14 +97,11 @@ export const pagedList = (command, { limit, page, all }, { items, has_more, ...r
|
|
|
97
97
|
/** The fields a list can be narrowed by: a provider list only those its source answers. */
|
|
98
98
|
const askable = (registry) => registry.backend === "native" ? registry.fields.filter((field) => field.native) : registry.fields;
|
|
99
99
|
/** An MCP list's `query` argument: the language the CLI list takes after its arguments. */
|
|
100
|
-
export const queryArgument = (registry) =>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
? "field:value terms joined by AND that narrow the list"
|
|
106
|
-
: "field:value terms that narrow the list (Lucene: AND OR NOT, [a TO b], > <)"}; fields: ${fields.map((field) => field.name).join(", ")}; e.g. ${fields[0]?.example ?? ""}`)));
|
|
107
|
-
};
|
|
100
|
+
export const queryArgument = (registry) => v.optional(v.pipe(v.string(), v.maxLength(QUERY_LIMITS.bytes), v.description(queryDescription(registry))));
|
|
101
|
+
/** The fields a native list can also leave out, `-field:value`: what `nativeTermLists` keeps under `-field`. */
|
|
102
|
+
export const negatableOf = (registry) => askable(registry)
|
|
103
|
+
.filter((field) => field.negatable)
|
|
104
|
+
.map((field) => field.name);
|
|
108
105
|
/**
|
|
109
106
|
* The terms of a query a provider answers: field:value joined by AND, each field once. Anything else is a
|
|
110
107
|
* question the source cannot ask, so it is refused rather than answered from a part of the list.
|
|
@@ -112,9 +109,10 @@ export const queryArgument = (registry) => {
|
|
|
112
109
|
export const nativeTerms = (ast, command) => new Map([...nativeTermLists(ast, command)].map(([field, [node]]) => [field, node]));
|
|
113
110
|
/**
|
|
114
111
|
* As `nativeTerms`, but each `repeatable` field may be named again, every term kept in order: `person:a person:b`
|
|
115
|
-
* asks for both, as AND reads.
|
|
112
|
+
* asks for both, as AND reads. A `negatable` field may also be excluded, `-field:value` or `NOT field:value`, as
|
|
113
|
+
* often as wanted; those terms are kept under `-field`.
|
|
116
114
|
*/
|
|
117
|
-
export const nativeTermLists = (ast, command, repeatable = []) => {
|
|
115
|
+
export const nativeTermLists = (ast, command, repeatable = [], negatable = []) => {
|
|
118
116
|
const terms = new Map();
|
|
119
117
|
if (!ast)
|
|
120
118
|
return terms;
|
|
@@ -123,8 +121,16 @@ export const nativeTermLists = (ast, command, repeatable = []) => {
|
|
|
123
121
|
const add = (node) => {
|
|
124
122
|
if (node.kind === "boolean") {
|
|
125
123
|
for (const clause of node.clauses) {
|
|
124
|
+
const excluded = clause.occur === "must_not" && clause.node.kind === "predicate" && negatable.includes(clause.node.field);
|
|
125
|
+
if (excluded && clause.node.kind === "predicate") {
|
|
126
|
+
const key = `-${clause.node.field}`;
|
|
127
|
+
terms.set(key, [...(terms.get(key) ?? []), clause.node]);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
126
130
|
if (clause.occur !== "must")
|
|
127
|
-
queryError("query.unsupported-placement", clause.node.span,
|
|
131
|
+
queryError("query.unsupported-placement", clause.node.span, negatable.length
|
|
132
|
+
? `this list takes only field:value terms joined by AND, and -field:value for ${negatable.join(", ")}`
|
|
133
|
+
: "this list takes only field:value terms joined by AND", context);
|
|
128
134
|
add(clause.node);
|
|
129
135
|
}
|
|
130
136
|
return;
|
|
@@ -3,9 +3,9 @@ import type { CacheDatabase } from "../../store/driver.js";
|
|
|
3
3
|
import type { Messenger } from "./context.js";
|
|
4
4
|
/** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
|
|
5
5
|
export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
|
|
6
|
+
error?: undefined;
|
|
6
7
|
path: string;
|
|
7
8
|
exists: boolean;
|
|
8
|
-
error?: undefined;
|
|
9
9
|
} | {
|
|
10
10
|
path: string;
|
|
11
11
|
exists: boolean;
|
package/dist/errors/reasons.js
CHANGED
|
@@ -62,6 +62,13 @@ const OTHERS = [
|
|
|
62
62
|
validation("event.repeat-split", "the series cannot be split at this occurrence, or was split there already"),
|
|
63
63
|
validation("event.repeat-count", "a COUNT series is split only with a rule for its new part"),
|
|
64
64
|
validation("event.unmovable", "the provider keeps this kind of event on its calendar"),
|
|
65
|
+
validation("event.text-with-fields", "--text takes the whole event from its line, so no field option goes with it"),
|
|
66
|
+
validation("calendar.ambiguous", "two or more calendars have this name; name one by its id"),
|
|
67
|
+
validation("export.too-many", "the query finds more events than one export writes"),
|
|
68
|
+
validation("ics.not-icalendar", "the file is not iCalendar"),
|
|
69
|
+
validation("ics.no-events", "the file holds no event that can be imported"),
|
|
70
|
+
validation("ics.too-many", "the file holds more events than one import sends"),
|
|
71
|
+
validation("ics.too-large", "the file is larger than one import reads"),
|
|
65
72
|
["calendar.read-only", "permission_error", "the calendar takes no new events from this account"],
|
|
66
73
|
["calendar.unsynced", "not_found", "the calendar account has no synced calendar in the store"],
|
|
67
74
|
["calendar.sync-incomplete", "not_found", "a synced calendar's sync never finished or is mid-round"],
|
package/dist/mcp/agent.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Readable } from "node:stream";
|
|
2
2
|
import { CliError, captureStreams, errorObjectOf } from "@wirecat/cli-base";
|
|
3
3
|
import { flatten, } from "@wirecat/cli-base/commands";
|
|
4
|
+
import { queryDescription } from "@wirecat/cli-base/query";
|
|
4
5
|
import { writes } from "@wirecat/cli-base/standards";
|
|
5
6
|
import { VOCABULARY } from "@wirecat/cli-base/standards/tables";
|
|
6
7
|
import * as v from "valibot";
|
|
7
8
|
import { commandAvailability } from "../cli/command-availability.js";
|
|
8
9
|
import { changesMessenger } from "../cli/effects.js";
|
|
10
|
+
import { QUERY_ARGUMENT } from "../cli/list-query.js";
|
|
9
11
|
import { lentConnection } from "../cli/messenger/serve-relay.js";
|
|
10
12
|
import { newRunId } from "../runs/run.js";
|
|
11
13
|
import { enclosingTraceId, inRun, traceOf } from "../runs/scope.js";
|
|
@@ -78,10 +80,17 @@ const optionField = (option, long, negated) => {
|
|
|
78
80
|
},
|
|
79
81
|
};
|
|
80
82
|
};
|
|
81
|
-
|
|
83
|
+
/** A list's query names its fields here: the help's Fields section, which its description points to, is not sent. */
|
|
84
|
+
const argumentField = (argument, query) => {
|
|
85
|
+
const own = argument.description || argument.name;
|
|
86
|
+
const about = query && argument.name === "query"
|
|
87
|
+
? own === QUERY_ARGUMENT
|
|
88
|
+
? queryDescription(query)
|
|
89
|
+
: `${own}; ${queryDescription(query)}`
|
|
90
|
+
: own;
|
|
82
91
|
const one = argument.choices
|
|
83
92
|
? v.pipe(v.picklist(argument.choices), v.description(argument.description))
|
|
84
|
-
: text(
|
|
93
|
+
: text(about);
|
|
85
94
|
return {
|
|
86
95
|
name: snake(argument.name),
|
|
87
96
|
required: argument.required,
|
|
@@ -109,7 +118,7 @@ const stdinField = ({ name, description, json }) => ({
|
|
|
109
118
|
argv: () => [],
|
|
110
119
|
});
|
|
111
120
|
const fieldsOf = (info, { omit = [], offline = false, stdin }) => {
|
|
112
|
-
const fields = info.arguments.map(argumentField);
|
|
121
|
+
const fields = info.arguments.map((argument) => argumentField(argument, info.query));
|
|
113
122
|
const read = stdin?.(info.path);
|
|
114
123
|
if (read)
|
|
115
124
|
fields.push(stdinField(read));
|
package/dist/mcp/app-server.js
CHANGED
|
@@ -4,7 +4,7 @@ import { CliError, mcpErrorResult } from "@wirecat/cli-base";
|
|
|
4
4
|
import { describeProgram } from "@wirecat/cli-base/commands";
|
|
5
5
|
import { skillResource } from "@wirecat/cli-base/skill";
|
|
6
6
|
import { environmentOf } from "../cli/context.js";
|
|
7
|
-
import { changesAuthorityAt,
|
|
7
|
+
import { changesAuthorityAt, judgedTargets, readsOverMcpAt } from "../cli/effects.js";
|
|
8
8
|
import { DEFAULT_OUTPUT_BYTES } from "../cli/execution.js";
|
|
9
9
|
import { rootOf } from "../cli/steps.js";
|
|
10
10
|
import { mcpUrlOf } from "../http/public-url.js";
|
|
@@ -46,7 +46,7 @@ export const createAppServer = (command, context, options) => {
|
|
|
46
46
|
currentLevels: levels,
|
|
47
47
|
readOnly: readOnly(),
|
|
48
48
|
authority,
|
|
49
|
-
fetchOnly:
|
|
49
|
+
fetchOnly: readsOverMcpAt(root),
|
|
50
50
|
permissions: options.permissions,
|
|
51
51
|
});
|
|
52
52
|
const availability = (one) => () => {
|