@wirecat/cli-base 0.70.0 → 0.72.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
@@ -8,6 +8,38 @@ Versions before 0.8.0 are in the [git tags](https://github.com/WireCatLabs/cli-b
8
8
  Every entry says what changed as a caller sees it, why, and what to watch for — the rules are
9
9
  [`docs/dev/CONVENTIONS.md`](docs/dev/CONVENTIONS.md#the-changelog).
10
10
 
11
+ ## 0.72.0 — 02.11.2026
12
+
13
+ ### Added
14
+
15
+ - **A native list can say which fields it can leave out (`negatable`).** `queryFieldsOf(schema, { negatable: [...] })`
16
+ marks a field the source can also exclude with `-field:value` or `NOT field:value`; `commands --json` carries
17
+ `negatable: true` on it, and `fieldsHelp` adds a `Leave out:` line. Before, only the list's own code knew, so an
18
+ agent learned about `-event_type:birthday` from prose alone.
19
+ - **`queryDescription(registry)`: a list's query in one sentence.** The terms it takes, which fields `-` can leave
20
+ out, and every field with its values: for an input that has no Fields section beside it, such as an MCP tool's
21
+ `query`.
22
+ - **`meta.now`, `meta.now_local` and `clockMeta(now, zone)` (WC-TIME-7).** An answer an agent plans by can carry the
23
+ time it was made, in UTC and on the wall clock of the zone in use, beside `meta.timezone`. `metaSchema` accepts
24
+ both keys; `localTime` is the schema of the second.
25
+ - **WC-CMD-17 and WC-MCP-4: a write that only an owner-only option asks for is a read over MCP.** `events export`
26
+ prints the `.ics` text, and writes a file only with `--output`, which MCP tools never offer; cli-core's
27
+ `writesFileOnlyWith` runs it through `read`, and the read-only lock refuses only a run that names a file.
28
+
29
+ ## 0.71.0 — 01.11.2026
30
+
31
+ ### Added
32
+
33
+ - **A vocabulary flag can list the value names other domains give it (`otherValues`).** `--to` stays `<chat>` and also
34
+ takes `<address>` (mail); `--subject` stays `<ref>` and also takes `<text>`. `flag-two-meanings` accepts those and
35
+ reports any other value name. A CLI that declares `--to <address>` or `--subject <text>` needs no override.
36
+
37
+ ### Fixed
38
+
39
+ - **0.70.0 made every messenger CLI fail `flag-two-meanings`.** It wrote `--to` as `<chat|address>` and `--subject`
40
+ as `<ref|text>`, and the lint compares the value name as written, so `--to <chat>` was reported. Both go back to
41
+ the original value name, and mail's names are listed beside them. Do not use 0.70.0.
42
+
11
43
  ## 0.70.0 — 01.11.2026
12
44
 
13
45
  ### Added
@@ -33,6 +33,7 @@ const fieldInfoSchema = v.object({
33
33
  relation: v.optional(v.string()),
34
34
  native: v.optional(v.boolean()),
35
35
  placement: v.optional(v.literal("and")),
36
+ negatable: v.optional(v.boolean()),
36
37
  });
37
38
  const commandInfoSchema = v.object({
38
39
  path: v.array(v.string()),
package/dist/dev/lint.js CHANGED
@@ -229,7 +229,10 @@ const CHECKS = {
229
229
  }));
230
230
  },
231
231
  "flag-two-meanings": (document, vocabulary) => {
232
- const registered = new Map(vocabulary.flags.map((flag) => [flag.name, flag.value?.slice(1, -1).toLowerCase() ?? "switch"]));
232
+ const registered = new Map(vocabulary.flags.map((flag) => [
233
+ flag.name,
234
+ [flag.value?.slice(1, -1).toLowerCase() ?? "switch", ...(flag.otherValues ?? [])],
235
+ ]));
233
236
  const uses = new Map();
234
237
  const note = (option, where) => {
235
238
  const name = longName(option.flags);
@@ -246,8 +249,8 @@ const CHECKS = {
246
249
  const found = [];
247
250
  for (const [name, kinds] of [...uses].sort(([a], [b]) => byText(a, b))) {
248
251
  const expected = registered.get(name);
249
- const off = expected === undefined ? [] : [...kinds.keys()].filter((kind) => kind !== expected);
250
- if (kinds.size < 2 && off.length === 0)
252
+ const off = expected === undefined ? [] : [...kinds.keys()].filter((kind) => !expected.includes(kind));
253
+ if (expected === undefined ? kinds.size < 2 : off.length === 0)
251
254
  continue;
252
255
  const described = [...kinds]
253
256
  .map(([kind, where]) => `${kind === "switch" ? "a switch" : `<${kind}>`} in ${where.slice(0, 3).join(", ")}`)
@@ -256,7 +259,7 @@ const CHECKS = {
256
259
  target: name,
257
260
  message: expected === undefined
258
261
  ? `${name} has ${kinds.size} meanings: ${described}`
259
- : `${name} is ${expected === "switch" ? "a switch" : `<${expected}>`} in the vocabulary, but: ${described}`,
262
+ : `${name} is ${expected[0] === "switch" ? "a switch" : expected.map((kind) => `<${kind}>`).join(" or ")} in the vocabulary, but: ${described}`,
260
263
  });
261
264
  }
262
265
  return found;
@@ -18,6 +18,8 @@ export interface FieldSpec {
18
18
  native?: boolean;
19
19
  /** `and`: only as a required term, never under OR or NOT — what a relation compiled to a sub-select needs. */
20
20
  placement?: "and";
21
+ /** On a native list, true when the source can also leave out what matches: `-field:value` or `NOT field:value`. */
22
+ negatable?: boolean;
21
23
  /** Checks only this field needs; throw with `queryError`. */
22
24
  validate?: (node: Predicate) => void;
23
25
  }
@@ -44,6 +46,8 @@ export interface FieldAnnotations {
44
46
  examples?: Readonly<Record<string, string>>;
45
47
  /** On a native list, the keys the source filters. */
46
48
  native?: readonly string[];
49
+ /** On a native list, the keys the source can also leave out: `-event_type:birthday`. */
50
+ negatable?: readonly string[];
47
51
  validate?: Readonly<Record<string, (node: Predicate) => void>>;
48
52
  }
49
53
  /**
@@ -54,11 +54,13 @@ export const queryFieldsOf = (schema, annotations = {}) => {
54
54
  const excluded = new Set(annotations.exclude ?? []);
55
55
  const words = new Set(annotations.text ?? []);
56
56
  const native = new Set(annotations.native ?? []);
57
+ const negatable = new Set(annotations.negatable ?? []);
57
58
  const finish = (field) => ({
58
59
  ...field,
59
60
  operators: OPERATORS_BY_TYPE[field.type],
60
61
  example: annotations.examples?.[field.name] ?? field.example ?? exampleOf(field.name, field.type, field.values),
61
62
  ...(native.has(field.name) ? { native: true } : {}),
63
+ ...(negatable.has(field.name) ? { negatable: true } : {}),
62
64
  ...(annotations.validate?.[field.name] ? { validate: annotations.validate[field.name] } : {}),
63
65
  });
64
66
  const own = Object.entries(entries).flatMap(([name, entry]) => {
@@ -2,3 +2,8 @@ import type { QueryRegistry } from "./fields.js";
2
2
  export declare const QUERY_SYNTAX = "field:value \u00B7 \"a phrase\" \u00B7 word* \u00B7 AND OR NOT \u00B7 ( ) \u00B7 [a TO b] \u00B7 > >= < <=";
3
3
  /** The Fields section of a list's help: one line per field it filters by, its defaults, the syntax and the guide. */
4
4
  export declare const fieldsHelp: (registry: Pick<QueryRegistry, "fields" | "defaults"> & Partial<Pick<QueryRegistry, "backend">>, guide?: string) => string;
5
+ /**
6
+ * A list's query in one sentence, for an input with no help page beside it (an MCP tool's `query`): the terms the
7
+ * list takes, which fields may be left out with `-`, and every field it filters by.
8
+ */
9
+ export declare const queryDescription: (registry: Pick<QueryRegistry, "fields"> & Partial<Pick<QueryRegistry, "backend" | "defaults">>) => string;
@@ -1,8 +1,11 @@
1
1
  export const QUERY_SYNTAX = 'field:value · "a phrase" · word* · AND OR NOT · ( ) · [a TO b] · > >= < <=';
2
+ const shownOf = (registry) => registry.backend === "native" ? registry.fields.filter((field) => field.native) : registry.fields;
3
+ const leftOut = (fields) => fields.filter((field) => field.negatable).map((field) => field.name);
2
4
  const kindOf = (field) => field.values ? `${field.type}: ${field.values.join("|")}` : `${field.type}: ${field.operators.join(", ")}`;
3
5
  /** The Fields section of a list's help: one line per field it filters by, its defaults, the syntax and the guide. */
4
6
  export const fieldsHelp = (registry, guide) => {
5
- const shown = registry.backend === "native" ? registry.fields.filter((field) => field.native) : registry.fields;
7
+ const shown = shownOf(registry);
8
+ const negatable = leftOut(shown);
6
9
  const rows = shown.map((field) => [field.name, kindOf(field), field.example]);
7
10
  const widths = [0, 1].map((at) => Math.max(...rows.map((row) => (row[at] ?? "").length)));
8
11
  return [
@@ -11,7 +14,28 @@ export const fieldsHelp = (registry, guide) => {
11
14
  ...(registry.defaults && registry.defaults.length > 0
12
15
  ? [`Defaults: ${registry.defaults.join(" ")} — name the field to change it`]
13
16
  : []),
17
+ ...(negatable.length > 0
18
+ ? [`Leave out: -${negatable[0]}:value or NOT ${negatable[0]}:value, for ${negatable.join(", ")}`]
19
+ : []),
14
20
  `Syntax: ${QUERY_SYNTAX}`,
15
21
  ...(guide ? [`Guide: ${guide}`] : []),
16
22
  ].join("\n");
17
23
  };
24
+ /**
25
+ * A list's query in one sentence, for an input with no help page beside it (an MCP tool's `query`): the terms the
26
+ * list takes, which fields may be left out with `-`, and every field it filters by.
27
+ */
28
+ export const queryDescription = (registry) => {
29
+ const shown = shownOf(registry);
30
+ if (shown.length === 0)
31
+ return "field:value terms; this list filters by none yet, so any term is refused";
32
+ const negatable = leftOut(shown);
33
+ const terms = registry.backend === "native"
34
+ ? `field:value terms joined by AND that narrow the list${negatable.length > 0
35
+ ? `; -field:value or NOT field:value leaves out what matches, for ${negatable.join(", ")} only`
36
+ : ""}`
37
+ : "field:value terms that narrow the list (Lucene: AND OR NOT, -field:value, [a TO b], > <)";
38
+ const fields = shown.map((field) => (field.values ? `${field.name} (${field.values.join("|")})` : field.name));
39
+ const defaults = registry.defaults?.length ? `; unless named: ${registry.defaults.join(" ")}` : "";
40
+ return `${terms}; fields: ${fields.join(", ")}; e.g. ${shown[0]?.example ?? ""}${defaults}`;
41
+ };
@@ -5,7 +5,7 @@
5
5
  export { type DateRange, dateEndpoint, dateRange, dayBoundary, timezoneOf, wallTime, zonedMoment } from "./dates.js";
6
6
  export { type EvaluateOptions, evaluate } from "./evaluate.js";
7
7
  export { FIELD_TYPES, type FieldAnnotations, type FieldSpec, type FieldType, OPERATORS_BY_TYPE, QUERY_BACKENDS, type QueryBackend, type QueryRegistry, queryFieldsOf, } from "./fields.js";
8
- export { fieldsHelp, QUERY_SYNTAX } from "./help.js";
8
+ export { fieldsHelp, QUERY_SYNTAX, queryDescription } from "./help.js";
9
9
  export { implicitTextTerms, type ParseOptions, parseLucene } from "./parser.js";
10
10
  export { defineQuery, describeQuery, exampleProblems, type FieldInfo, QUERY_REASONS, type QueryInfo, } from "./registry.js";
11
11
  export { type Occur, type Operator, type Predicate, QUERY_LIMITS, type QueryAst, type QueryContext, type QueryNode, queryAstSchema, queryError, queryNodeSchema, type Span, walkQuery, } from "./types.js";
@@ -5,7 +5,7 @@
5
5
  export { dateEndpoint, dateRange, dayBoundary, timezoneOf, wallTime, zonedMoment } from "./dates.js";
6
6
  export { evaluate } from "./evaluate.js";
7
7
  export { FIELD_TYPES, OPERATORS_BY_TYPE, QUERY_BACKENDS, queryFieldsOf, } from "./fields.js";
8
- export { fieldsHelp, QUERY_SYNTAX } from "./help.js";
8
+ export { fieldsHelp, QUERY_SYNTAX, queryDescription } from "./help.js";
9
9
  export { implicitTextTerms, parseLucene } from "./parser.js";
10
10
  export { defineQuery, describeQuery, exampleProblems, QUERY_REASONS, } from "./registry.js";
11
11
  export { QUERY_LIMITS, queryAstSchema, queryError, queryNodeSchema, walkQuery, } from "./types.js";
@@ -444,10 +444,11 @@ export const WHAT_FLAGS = [
444
444
  { name: "--task", value: "<id>", role: "target", type: "id", meaning: "a task, by id" },
445
445
  {
446
446
  name: "--to",
447
- value: "<chat|address>",
447
+ value: "<chat>",
448
448
  role: "target",
449
- type: "string",
450
- meaning: "who it goes to: a chat in a messenger, an e-mail address in mail; repeat it for more addresses",
449
+ type: "id",
450
+ otherValues: ["address"],
451
+ meaning: "who it goes to: a chat in a messenger (<chat>), an e-mail address in mail (<address>, repeated for more)",
451
452
  },
452
453
  {
453
454
  name: "--topic-name",
@@ -895,10 +896,11 @@ export const WHAT_FLAGS = [
895
896
  { name: "--sticker", value: "<id>", role: "value", type: "id", meaning: "send this sticker, alone" },
896
897
  {
897
898
  name: "--subject",
898
- value: "<ref|text>",
899
+ value: "<ref>",
899
900
  role: "value",
900
901
  type: "string",
901
- meaning: "what it is about: a reference in a note or task, the subject line in mail",
902
+ otherValues: ["text"],
903
+ meaning: "what it is about: a reference in a note or task (<ref>), the subject line in mail (<text>)",
902
904
  replaces: ["--about"],
903
905
  },
904
906
  {
@@ -1,6 +1,6 @@
1
1
  export { EFFECT_LEVELS, type EffectLevel, type Effects, effectsSchema, writes } from "./effects.js";
2
2
  export { BASE_REASONS, COMMON_DETAILS, CRASH_CODE, CRASH_REASON, defineReasons, type ErrorObject, type ErrorReason, errorObjectSchema, } from "./errors.js";
3
3
  export { defineLogEvents, isContentKey, LOG_ATTRIBUTE_KEYS, LOG_EVENTS, LOG_SEVERITIES, type LogEvent, type LogRecord, type LogSeverity, logAttributesJsonSchema, logEventProblems, logRecordSchema, traceIdOf, } from "./log.js";
4
- export { type Issue, type ItemEnvelope, isoTime, issueSchema, itemEnvelopeSchema, jsonlEndSchema, jsonlItemSchema, jsonlOf, type ListEnvelope, listEnvelopeSchema, MAX_NEXT_STEPS, type Meta, type Missing, metaSchema, missingSchema, type NextStep, nextStepSchema, type Page, pageSchema, reference, type Total, totalSchema, } from "./output.js";
4
+ export { clockMeta, type Issue, type ItemEnvelope, isoTime, issueSchema, itemEnvelopeSchema, jsonlEndSchema, jsonlItemSchema, jsonlOf, type ListEnvelope, listEnvelopeSchema, localTime, MAX_NEXT_STEPS, type Meta, type Missing, metaSchema, missingSchema, type NextStep, nextStepSchema, type Page, pageSchema, reference, type Total, totalSchema, } from "./output.js";
5
5
  export { fill, jsonSchemaOf, RegistryError } from "./registry.js";
6
6
  export { defineSteps, SAME_COMMAND, STEPS, type StepDefinition, type StepInput, type StepTarget, stepOf, } from "./steps.js";
@@ -1,6 +1,6 @@
1
1
  export { EFFECT_LEVELS, effectsSchema, writes } from "./effects.js";
2
2
  export { BASE_REASONS, COMMON_DETAILS, CRASH_CODE, CRASH_REASON, defineReasons, errorObjectSchema, } from "./errors.js";
3
3
  export { defineLogEvents, isContentKey, LOG_ATTRIBUTE_KEYS, LOG_EVENTS, LOG_SEVERITIES, logAttributesJsonSchema, logEventProblems, logRecordSchema, traceIdOf, } from "./log.js";
4
- export { isoTime, issueSchema, itemEnvelopeSchema, jsonlEndSchema, jsonlItemSchema, jsonlOf, listEnvelopeSchema, MAX_NEXT_STEPS, metaSchema, missingSchema, nextStepSchema, pageSchema, reference, totalSchema, } from "./output.js";
4
+ export { clockMeta, isoTime, issueSchema, itemEnvelopeSchema, jsonlEndSchema, jsonlItemSchema, jsonlOf, listEnvelopeSchema, localTime, MAX_NEXT_STEPS, metaSchema, missingSchema, nextStepSchema, pageSchema, reference, totalSchema, } from "./output.js";
5
5
  export { fill, jsonSchemaOf, RegistryError } from "./registry.js";
6
6
  export { defineSteps, SAME_COMMAND, STEPS, stepOf, } from "./steps.js";
@@ -2,6 +2,8 @@ import * as v from "valibot";
2
2
  /** At most two next steps per answer: the one to take, then one alternative. */
3
3
  export declare const MAX_NEXT_STEPS = 2;
4
4
  export declare const isoTime: 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`">]>;
5
+ /** The same moment on the wall clock of a zone, with its offset: `2026-11-02T16:03:00.000+01:00` (WC-TIME-2). */
6
+ export declare const localTime: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "an ISO 8601 time with its offset">]>;
5
7
  /** The typed name of a stored thing, `msg:telegram/1/2/3` or `note:42`: what every other command takes. */
6
8
  export declare const reference: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
7
9
  export declare const nextStepSchema: v.VariantSchema<"kind", [v.StrictObjectSchema<{
@@ -83,11 +85,23 @@ export declare const metaSchema: v.StrictObjectSchema<{
83
85
  }, undefined>, undefined>, undefined>;
84
86
  readonly truncated: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
85
87
  readonly timezone: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
88
+ /** When the command answered, for an agent that plans from "now" (WC-TIME-7); `timezone` names the zone of `now_local`. */
89
+ 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>;
90
+ readonly now_local: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "an ISO 8601 time with its offset">]>, undefined>;
86
91
  readonly window: v.OptionalSchema<v.StrictObjectSchema<{
87
92
  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>;
88
93
  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>;
89
94
  }, undefined>, undefined>;
90
95
  }, undefined>;
96
+ /**
97
+ * `meta.now`, `meta.now_local` and `meta.timezone` for an answer an agent plans by (WC-TIME-7): the moment in UTC, the
98
+ * same moment on the zone's wall clock, and the zone.
99
+ */
100
+ export declare const clockMeta: (at: number, zone: string) => {
101
+ now: string;
102
+ now_local: string;
103
+ timezone: string;
104
+ };
91
105
  export declare const listEnvelopeSchema: <T extends v.GenericSchema>(item: T) => v.StrictObjectSchema<{
92
106
  readonly items: v.ArraySchema<T, undefined>;
93
107
  readonly page: v.VariantSchema<"mode", [v.StrictObjectSchema<{
@@ -163,6 +177,9 @@ export declare const listEnvelopeSchema: <T extends v.GenericSchema>(item: T) =>
163
177
  }, undefined>, undefined>, undefined>;
164
178
  readonly truncated: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
165
179
  readonly timezone: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
180
+ /** When the command answered, for an agent that plans from "now" (WC-TIME-7); `timezone` names the zone of `now_local`. */
181
+ 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>;
182
+ readonly now_local: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "an ISO 8601 time with its offset">]>, undefined>;
166
183
  readonly window: v.OptionalSchema<v.StrictObjectSchema<{
167
184
  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>;
168
185
  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>;
@@ -225,6 +242,9 @@ export declare const itemEnvelopeSchema: <T extends v.GenericSchema>(item: T) =>
225
242
  }, undefined>, undefined>, undefined>;
226
243
  readonly truncated: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
227
244
  readonly timezone: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
245
+ /** When the command answered, for an agent that plans from "now" (WC-TIME-7); `timezone` names the zone of `now_local`. */
246
+ 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>;
247
+ readonly now_local: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "an ISO 8601 time with its offset">]>, undefined>;
228
248
  readonly window: v.OptionalSchema<v.StrictObjectSchema<{
229
249
  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>;
230
250
  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>;
@@ -4,6 +4,8 @@ import { dottedId, text } from "./registry.js";
4
4
  /** At most two next steps per answer: the one to take, then one alternative. */
5
5
  export const MAX_NEXT_STEPS = 2;
6
6
  export const isoTime = v.pipe(v.string(), v.isoTimestamp("an ISO 8601 time in UTC"), v.endsWith("Z", "an ISO 8601 time in UTC, ending in `Z`"));
7
+ /** The same moment on the wall clock of a zone, with its offset: `2026-11-02T16:03:00.000+01:00` (WC-TIME-2). */
8
+ export const localTime = v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/, "an ISO 8601 time with its offset"));
7
9
  /** The typed name of a stored thing, `msg:telegram/1/2/3` or `note:42`: what every other command takes. */
8
10
  export const reference = v.pipe(v.string(), v.regex(/^[a-z]+:\S+$/, "a reference such as `note:42`"));
9
11
  export const nextStepSchema = v.variant("kind", [
@@ -81,8 +83,28 @@ export const metaSchema = v.strictObject({
81
83
  issues: v.optional(v.array(issueSchema)),
82
84
  truncated: v.optional(v.boolean()),
83
85
  timezone: v.optional(text),
86
+ /** When the command answered, for an agent that plans from "now" (WC-TIME-7); `timezone` names the zone of `now_local`. */
87
+ now: v.optional(isoTime),
88
+ now_local: v.optional(localTime),
84
89
  window: v.optional(v.strictObject({ since: v.optional(isoTime), until: v.optional(isoTime) })),
85
90
  });
91
+ const offsetOf = (at, zone) => {
92
+ const name = new Intl.DateTimeFormat("en-US", { timeZone: zone, timeZoneName: "longOffset" })
93
+ .formatToParts(at)
94
+ .find((part) => part.type === "timeZoneName")?.value ?? "GMT";
95
+ return name === "GMT" ? "+00:00" : name.slice(3);
96
+ };
97
+ /**
98
+ * `meta.now`, `meta.now_local` and `meta.timezone` for an answer an agent plans by (WC-TIME-7): the moment in UTC, the
99
+ * same moment on the zone's wall clock, and the zone.
100
+ */
101
+ export const clockMeta = (at, zone) => {
102
+ const offset = offsetOf(at, zone);
103
+ const sign = offset.startsWith("-") ? -1 : 1;
104
+ const [hours = 0, minutes = 0] = offset.slice(1).split(":").map(Number);
105
+ const wall = new Date(at + sign * (hours * 60 + minutes) * 60_000).toISOString().slice(0, -1);
106
+ return { now: new Date(at).toISOString(), now_local: `${wall}${offset}`, timezone: zone };
107
+ };
86
108
  const nextSteps = v.optional(v.pipe(v.array(nextStepSchema), v.maxLength(MAX_NEXT_STEPS)));
87
109
  export const listEnvelopeSchema = (item) => v.strictObject({
88
110
  items: v.array(item),
@@ -43,6 +43,11 @@ export declare const vocabularySchema: v.StrictObjectSchema<{
43
43
  readonly type: v.PicklistSchema<readonly ["switch", "count", "number", "string", "id", "time", "duration", "choice", "list", "path", "cursor"], undefined>;
44
44
  readonly role: v.PicklistSchema<["action" | "budget" | "mode" | "output" | "paging" | "query" | "scope" | "sort" | "target" | "value", ...("action" | "budget" | "mode" | "output" | "paging" | "query" | "scope" | "sort" | "target" | "value")[]], undefined>;
45
45
  readonly meaning: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
46
+ /**
47
+ * Value names a domain other than the main one gives the flag, such as mail's `--to <address>` beside the
48
+ * messengers' `<chat>`. The meaning for each says which is which. `flag-two-meanings` accepts these and no others.
49
+ */
50
+ readonly otherValues: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a value name such as address">]>, undefined>, undefined>;
46
51
  readonly unit: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
47
52
  /** Other names for the same flag, which `flag-synonym` refuses. */
48
53
  readonly replaces: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a long flag">]>, undefined>, undefined>;
@@ -49,6 +49,11 @@ const flagSchema = v.strictObject({
49
49
  type: v.picklist(FLAG_TYPES),
50
50
  role: v.picklist(Object.keys(FLAG_ROLES)),
51
51
  meaning: text,
52
+ /**
53
+ * Value names a domain other than the main one gives the flag, such as mail's `--to <address>` beside the
54
+ * messengers' `<chat>`. The meaning for each says which is which. `flag-two-meanings` accepts these and no others.
55
+ */
56
+ otherValues: v.optional(v.array(v.pipe(v.string(), v.regex(/^[a-z][a-z0-9-]*$/, "a value name such as address")))),
52
57
  unit: v.optional(text),
53
58
  /** Other names for the same flag, which `flag-synonym` refuses. */
54
59
  replaces: v.optional(v.array(v.pipe(v.string(), v.regex(/^--[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "a long flag")))),
@@ -44,7 +44,7 @@ the four steps that need a person. A new CLI is ready for its first release when
44
44
  | [Comparisons](comparisons.md) | `CMP` | 11 | Which tools a comparison page lists, rows as user tasks, agents and safety, where others are better, proof of marks |
45
45
  | [Releases](releases.md) | `REL` | 15 | `bin/release`, the release lock, versions, the owner's yes, trusted publishing |
46
46
  | [CI](ci.md) | `CI` | 12 | Hooks, the pull request budget, full validation, monthly runs |
47
- | [Time](time.md) | `TIME` | 6 | UTC in the store and in answers, the zone at input and display edges, date-only filters, tests in another zone |
47
+ | [Time](time.md) | `TIME` | 7 | UTC in the store and in answers, the zone at input and display edges, date-only filters, tests in another zone, the time in answers agents plan by |
48
48
  | [Style](style.md) | `STYLE` | 10 | Comments, TypeScript habits, commit messages |
49
49
  | [New CLI](new-cli.md) | — | — | Starting a new CLI from the template: the command, the four manual steps, the checks |
50
50
  | [Kit surface](kit-surface.md) | — | — | The cli-base and cli-core exports the template is built on, and how a change to one reaches you |
@@ -214,7 +214,10 @@ store or files without reading the service first (`remote: "none"`, `local: "wri
214
214
  - **Exceptions:** none. Without `effects`, the error handler never marks a failure safe to retry, and a next step
215
215
  that runs the command is never safe to run unasked. cli-base's `writes()` (`src/standards/effects.ts`) still
216
216
  counts every local write as a write; cli-core's MCP router adds its own `fetchesOnly` test (cli-core 0.468.0), so
217
- `sync` and `calendars list` run through `read`.
217
+ `sync` and `calendars list` run through `read`. A command whose only local write is the file an owner-only option
218
+ names (`mcp: false`, such as `events export --output`) stays a write, and cli-core's `writesFileOnlyWith` marks it:
219
+ an MCP tool never offers the option, so it runs through `read`, and `<PREFIX>_READ_ONLY` refuses only a run that
220
+ gives the option.
218
221
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0 (what counts as a read: cli-base 0.65.0). **Rulings:** the
219
222
  owner's ruling on what counts as a read over MCP (2026-10-27).
220
223
 
@@ -80,6 +80,8 @@ A command that reads the service and only saves what it read into the owner's lo
80
80
  - **Enforced by:** `cli-core: src/mcp/mcp.test.ts` · `cli-core: src/mcp/output-schema.test.ts`.
81
81
  - **Example:** `max_read { "command": "messages send", … }` is refused. `gcal_read { "command": "sync" }` runs:
82
82
  cli-core counts a command that only fetches into the local store as a read (`fetchesOnly`, cli-core 0.468.0).
83
+ `gcal_read { "command": "events export", … }` runs too: its one write is the file `--output` names, and MCP tools
84
+ never offer `--output` (cli-core's `writesFileOnlyWith`).
83
85
  - **Exceptions:** none.
84
86
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0 (what counts as a read: cli-base 0.65.0). **Rulings:** the
85
87
  owner's ruling on what counts as a read over MCP (2026-10-27).
@@ -98,6 +98,22 @@ repository that stores times has a test that writes the same input under two zon
98
98
  - **Exceptions:** a repository that keeps no time skips the second test.
99
99
  - **Applies to:** all CLIs, cli-core and cli-base. **Since:** cli-base 0.48.0. **Rulings:** none.
100
100
 
101
+ ### WC-TIME-7 · Give the time and the zone in use where an agent plans from "now"
102
+
103
+ An answer an agent reads to plan or schedule — a calendar's events, a meeting's preparation, a setup check — carries
104
+ `meta.now` (the moment it answered, in UTC), `meta.now_local` (the same moment with the offset of the zone in use) and
105
+ `meta.timezone`. cli-base's `clockMeta(now, zone)` builds the three from the command's clock.
106
+
107
+ - **Why:** a model does not know the time, and guesses "today" and "next Monday" from its training. A widely used Google
108
+ Calendar MCP server gives its agents a `get-current-time` tool for the same reason; an answer that already carries
109
+ the time needs no extra call.
110
+ - **Enforced by:** review only.
111
+ - **Example:** `gcal personal events list --json` answers with `"meta": { "now": "2026-11-02T15:03:00.000Z",
112
+ "now_local": "2026-11-02T16:03:00.000+01:00", "timezone": "Europe/Madrid", … }`.
113
+ - **Exceptions:** an answer about something other than the owner's time (a list of profiles, a tool catalogue)
114
+ leaves them out. The clock is the command's injected one, so a test pins it.
115
+ - **Applies to:** all CLIs. **Since:** cli-base 0.72.0. **Rulings:** none.
116
+
101
117
  ## Sources
102
118
 
103
119
  - The owner's ruling of 21.10.2026: every time is stored and exchanged in UTC, in every repository.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-base",
3
- "version": "0.70.0",
3
+ "version": "0.72.0",
4
4
  "description": "The parts every command line tool needs: output modes, terminal rendering, error model, exit codes, credentials, clocks",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",