@wirecat/cli-core 0.539.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 CHANGED
@@ -7,6 +7,29 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.540.0 — 02.11.2026
11
+
12
+ ### Added
13
+
14
+ - **`events export` without `--output` is a read over MCP and under the read-only lock.** It only prints the `.ics`
15
+ text, and MCP tools never offer `--output`. `writesFileOnlyWith(command, option)` marks a command whose only local
16
+ write is the file an owner-only option names: its effects stay a write (WC-CMD-17), `readsOverMcp` / `readsOverMcpAt`
17
+ count it as a read, and `<PREFIX>_READ_ONLY` refuses only a run that gives the option. Every MCP server now judges
18
+ `read` with `readsOverMcpAt`, which is `fetchesOnly` plus these; a CLI test that snapshots the catalogue with
19
+ `fetchesOnlyAt` should switch to it.
20
+ - **An MCP list's `query` input names its fields.** It said "the fields are below", and no Fields section is sent
21
+ over MCP. It now reads cli-base's `queryDescription`: the fields, enum values, and which fields `-field:value`
22
+ leaves out. `events list`'s query declares `event_type` as `negatable` (cli-base), and `negatableOf(registry)`
23
+ gives `nativeTermLists` the same list, so the description and the filter cannot drift. `queryArgument` uses the
24
+ same sentence.
25
+ - **`events list` and `events prepare` give `meta.now`, `meta.now_local` and `meta.timezone` (WC-TIME-7)**, from the
26
+ host's clock and zone, so an agent plans "tomorrow" from the real date.
27
+
28
+ ### Changed — may break callers
29
+
30
+ - **cli-base 0.72.0.** `metaSchema` accepts `now` and `now_local`; a host that checks meta against an older cli-base's
31
+ strict schema drops the whole meta.
32
+
10
33
  ## 0.539.0 — 01.11.2026
11
34
 
12
35
  ### Added
@@ -1,10 +1,11 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { annotate, sharedCommand } from "@wirecat/cli-base/commands";
3
3
  import { timezoneOf } from "@wirecat/cli-base/query";
4
+ import { clockMeta } from "@wirecat/cli-base/standards";
4
5
  import { Argument, InvalidArgumentError, Option } from "commander";
5
6
  import { environmentOf, runBudgetOf } from "../../cli/context.js";
6
7
  import { honoursDryRun } from "../../cli/dry-run-guard.js";
7
- import { ownerOnlyOption } from "../../cli/effects.js";
8
+ import { ownerOnlyOption, writesFileOnlyWith } from "../../cli/effects.js";
8
9
  import { queryTerms, withQuery } from "../../cli/list-query.js";
9
10
  import { terminalAsker } from "../../cli/messenger/ask.js";
10
11
  import { lineOf } from "../../cli/steps.js";
@@ -235,7 +236,7 @@ const addCalendarCommandsDefinition = (program, deps) => {
235
236
  });
236
237
  print({
237
238
  ...eachMarked(await eventsAnswer(deps.store, filter && { ...filter, limit: opts.limit }, opts.page), EVENT_ITEM_UNTRUSTED),
238
- meta: await accountsMeta(deps.store, accounts, skipped, now()),
239
+ meta: { ...(await accountsMeta(deps.store, accounts, skipped, now())), ...clockMeta(now(), zone()) },
239
240
  }, command);
240
241
  });
241
242
  const exporter = example(withQuery(annotate(events.command("export"), { effects: deps.writeFile ? LOCAL_WRITE : LOCAL_READ }).description(`write stored events as iCalendar (.ics), at most ${EXPORT_MAX}: one with id:<id>, or those the query ` +
@@ -247,7 +248,7 @@ const addCalendarCommandsDefinition = (program, deps) => {
247
248
  : ["events export id:42", "events export starts_at:[2026-11-02 TO 2026-11-09} NOT event_type:birthday"]));
248
249
  // Not over MCP: an agent would choose where on the owner's disk to write. It gets the text in the answer.
249
250
  if (deps.writeFile)
250
- exporter.addOption(ownerOnlyOption("--output <file>", "the file to write; printed in the answer when absent"));
251
+ writesFileOnlyWith(exporter.addOption(ownerOnlyOption("--output <file>", "the file to write; printed in the answer when absent")), "output");
251
252
  exporter.action(async (words, opts, command) => {
252
253
  const filter = await eventsFilter(words, {
253
254
  store: deps.store,
@@ -297,7 +298,7 @@ const addCalendarCommandsDefinition = (program, deps) => {
297
298
  ...(opts.limit === undefined ? {} : { limit: opts.limit }),
298
299
  ...(window.since === undefined ? {} : { since: window.since }),
299
300
  });
300
- print({ ...prepared, meta: { ...meta, ...window.meta } }, command);
301
+ print({ ...prepared, meta: { ...meta, ...window.meta, ...clockMeta(now(), zone()) } }, command);
301
302
  });
302
303
  const searches = annotate(program.command("search").description("find stored things by text: events"), {
303
304
  effects: GROUP,
@@ -30,7 +30,8 @@ export const calendarTools = [
30
30
  name: "events_list",
31
31
  description: "stored events, earliest first; `query` takes field terms joined by AND: starts_at (>=, <, or " +
32
32
  "[a TO b}), ends_at>=, changed_at>= (events new, moved or cancelled since, each with change and was), " +
33
- "overlapping:true (timed events that clash, each with overlaps), calendar:<id>; a date in the zone given, " +
33
+ "overlapping:true (timed events that clash, each with overlaps), calendar:<id>, event_type:<kind>, and " +
34
+ "-event_type:<kind> to leave one kind out; a date in the zone given, " +
34
35
  "UTC when absent; without starts_at or ends_at, from today on",
35
36
  properties: {
36
37
  account_id: integer,
@@ -126,6 +126,8 @@ export declare const eventPreparation: (details: EventDetails, options: Preparat
126
126
  }[] | undefined;
127
127
  truncated?: boolean | undefined;
128
128
  timezone?: string | undefined;
129
+ now?: string | undefined;
130
+ now_local?: string | undefined;
129
131
  window?: {
130
132
  since?: string | undefined;
131
133
  until?: string | undefined;
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { dateRange, dayBoundary, queryError, queryFieldsOf, } from "@wirecat/cli-base/query";
3
- import { LIST_QUERY_GUIDE, listQuery, nativeTermLists, typedQuery } from "../cli/list-query.js";
3
+ import { LIST_QUERY_GUIDE, listQuery, nativeTermLists, negatableOf, typedQuery, } from "../cli/list-query.js";
4
4
  import { event, eventChange, eventItem, listed, } from "../meetings/index.js";
5
5
  import { entriesOfList, fitting } from "../search/account-list.js";
6
6
  import { listContextOf } from "../services/search-kind.js";
@@ -92,6 +92,7 @@ export const EVENTS_QUERY = {
92
92
  backend: "native",
93
93
  fields: queryFieldsOf(event, {
94
94
  native: ["id", "starts_at", "ends_at", "changed_at", "overlapping", "calendar", "person", "event_type"],
95
+ negatable: ["event_type"],
95
96
  relations: {
96
97
  event_type: {
97
98
  type: "enum",
@@ -156,7 +157,7 @@ export const eventsFilter = async (query, context) => {
156
157
  const { zone, now } = context;
157
158
  const span = (text) => ({ start: 0, end: text.length });
158
159
  const day = (date) => dayBoundary(date, zone, false, span(date));
159
- const terms = nativeTermLists(listQuery(typedQuery(query), EVENTS_QUERY, LIST), LIST, [], ["event_type"]);
160
+ const terms = nativeTermLists(listQuery(typedQuery(query), EVENTS_QUERY, LIST), LIST, [], negatableOf(EVENTS_QUERY));
160
161
  const kinds = (nodes = []) => nodes.map(({ value }) => value.toLowerCase());
161
162
  const excluded = kinds(terms.get("-event_type"));
162
163
  if (excluded.length)
@@ -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, fetchesOnlyAt, LOCAL_READ, REMOTE_WRITE } from "../effects.js";
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: fetchesOnlyAt(rootOf(this)),
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,
@@ -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"`)
@@ -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. */
@@ -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>;
@@ -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";
@@ -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, "field:value terms; this list filters by none yet, so any term is refused" | `field:value terms joined by AND that narrow the list; fields: ${string}; e.g. ${string}` | `field:value terms that narrow the list (Lucene: AND OR NOT, [a TO b], > <); fields: ${string}; e.g. ${string}`>]>, undefined>;
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.
@@ -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
- const fields = askable(registry);
102
- return v.optional(v.pipe(v.string(), v.maxLength(QUERY_LIMITS.bytes), v.description(fields.length === 0
103
- ? "field:value terms; this list filters by none yet, so any term is refused"
104
- : `${registry.backend === "native"
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.
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
- const argumentField = (argument) => {
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(argument.description || argument.name);
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));
@@ -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, fetchesOnlyAt, judgedTargets } from "../cli/effects.js";
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: fetchesOnlyAt(root),
49
+ fetchOnly: readsOverMcpAt(root),
50
50
  permissions: options.permissions,
51
51
  });
52
52
  const availability = (one) => () => {
@@ -8,7 +8,7 @@ import * as v from "valibot";
8
8
  import { findCommand } from "../cli/command-contract.js";
9
9
  import { environmentOf } from "../cli/context.js";
10
10
  import { refuseWriteBeforeConnecting } from "../cli/early-refusal.js";
11
- import { changesAuthorityAt, fetchesOnlyAt, judgedTargets } from "../cli/effects.js";
11
+ import { changesAuthorityAt, judgedTargets, readsOverMcpAt } from "../cli/effects.js";
12
12
  import { itemOf } from "../cli/envelope.js";
13
13
  import { DEFAULT_OUTPUT_BYTES } from "../cli/execution.js";
14
14
  import { recalledAccount } from "../cli/messenger/accounts.js";
@@ -127,7 +127,7 @@ export const createServer = (command, context, messenger, sessionOptions) => {
127
127
  readOnly: readOnlyLockOf(app, context.env) !== undefined,
128
128
  keyOf,
129
129
  authority,
130
- fetchOnly: fetchesOnlyAt(root),
130
+ fetchOnly: readsOverMcpAt(root),
131
131
  offline: true,
132
132
  stdin,
133
133
  }).map((one) => "sync_first" in one.input.entries && !syncAllowed(one.path)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.539.0",
3
+ "version": "0.540.0",
4
4
  "description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",
@@ -251,7 +251,7 @@
251
251
  "@types/node": "^22.20.5",
252
252
  "@types/pngjs": "6.0.5",
253
253
  "@vitest/coverage-v8": "^5.0.5",
254
- "@wirecat/cli-base": "0.71.0",
254
+ "@wirecat/cli-base": "0.72.0",
255
255
  "@wirecat/cli-core-legacy-tags": "npm:@wirecat/cli-core@0.414.0",
256
256
  "commander": "15.0.0",
257
257
  "cspell": "10.4.0",
@@ -271,7 +271,7 @@
271
271
  },
272
272
  "peerDependencies": {
273
273
  "@napi-rs/canvas": ">=0.1.69 <2",
274
- "@wirecat/cli-base": ">=0.71.0 <0.72.0",
274
+ "@wirecat/cli-base": ">=0.72.0 <0.73.0",
275
275
  "commander": ">=15"
276
276
  },
277
277
  "bin": {