@wirecat/cli-base 0.71.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 +18 -0
- package/dist/commands/document.js +1 -0
- package/dist/query/fields.d.ts +4 -0
- package/dist/query/fields.js +2 -0
- package/dist/query/help.d.ts +5 -0
- package/dist/query/help.js +25 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/standards/index.d.ts +1 -1
- package/dist/standards/index.js +1 -1
- package/dist/standards/output.d.ts +20 -0
- package/dist/standards/output.js +22 -0
- package/docs/standards/README.md +1 -1
- package/docs/standards/commands.md +4 -1
- package/docs/standards/mcp.md +2 -0
- package/docs/standards/time.md +16 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,24 @@ 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
|
+
|
|
11
29
|
## 0.71.0 — 01.11.2026
|
|
12
30
|
|
|
13
31
|
### 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/query/fields.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/query/fields.js
CHANGED
|
@@ -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]) => {
|
package/dist/query/help.d.ts
CHANGED
|
@@ -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;
|
package/dist/query/help.js
CHANGED
|
@@ -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
|
|
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
|
+
};
|
package/dist/query/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/query/index.js
CHANGED
|
@@ -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";
|
|
@@ -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";
|
package/dist/standards/index.js
CHANGED
|
@@ -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>;
|
package/dist/standards/output.js
CHANGED
|
@@ -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),
|
package/docs/standards/README.md
CHANGED
|
@@ -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` |
|
|
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
|
|
package/docs/standards/mcp.md
CHANGED
|
@@ -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).
|
package/docs/standards/time.md
CHANGED
|
@@ -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.
|
|
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",
|