@wirecat/cli-base 0.62.0 → 0.64.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.
@@ -9,7 +9,7 @@ export interface StoredCredential {
9
9
  export interface CredentialsOptions {
10
10
  /** Where the fallback file lives when the keyring cannot be used. */
11
11
  configDir: string;
12
- /** The keyring service name — the command's name, e.g. `max-cli`. */
12
+ /** The keyring service name — the CLI's app name, e.g. `max-cli`, not its command word. */
13
13
  service: string;
14
14
  /**
15
15
  * **True when the config directory came from the environment rather than the OS convention.**
@@ -0,0 +1,25 @@
1
+ export interface BundleOptions {
2
+ /** The CLI's root: `dist/` and `package.json` are under it. */
3
+ root: string;
4
+ /** The compiled bin, relative to `root`, e.g. `dist/bin/tg.js`. It becomes the loader. */
5
+ bin: string;
6
+ /** Packages that stay outside the bundle, besides `DEFAULT_EXTERNAL`. `*` matches anything. */
7
+ external?: readonly string[];
8
+ /** Packages whose modules keep their installed place, besides `DEFAULT_ANCHORED`. */
9
+ anchored?: readonly string[];
10
+ /** Third-party files read and found harmless, besides `DEFAULT_HARMLESS` (a path suffix each). */
11
+ harmless?: readonly string[];
12
+ }
13
+ export interface BundleResult {
14
+ problems: string[];
15
+ /** The chunks a server preloads, as the loader names them. */
16
+ chunks: string[];
17
+ }
18
+ /** Native modules, Bun's SQLite, and what cli-core loads only by name or starts as a worker from its own file. */
19
+ export declare const DEFAULT_EXTERNAL: string[];
20
+ export declare const DEFAULT_ANCHORED: string[];
21
+ export declare const DEFAULT_HARMLESS: string[];
22
+ export declare const LOADER_MARK = "// cli-dev bundle loader\n";
23
+ /** The bin that replaces the compiled one: `name` is the bundle's entry, `chunks` what a server preloads. */
24
+ export declare const loaderSource: (name: string, version: string, chunks: readonly string[]) => string;
25
+ export declare const bundle: (options: BundleOptions) => Promise<BundleResult>;
@@ -0,0 +1,271 @@
1
+ /**
2
+ * `cli-dev bundle`: the published bin starts from an esbuild bundle of `dist/`, so a command loads a few dozen files
3
+ * instead of about a thousand. Moved from tg-cli's `scripts/bundle.ts`, where it was proven first.
4
+ *
5
+ * - `dist/bundle/` holds the bundle. Its chunks keep every lazy `import()` lazy.
6
+ * - The bin becomes a loader: it answers `--version`, turns the compile cache on before any of the bundle compiles,
7
+ * and for a server preloads every chunk so an in-place upgrade cannot pull one from under it.
8
+ * - A module moved into a chunk would look for files and packages from the chunk. In the CLI's own modules and in the
9
+ * anchored packages (cli-core, cli-base), `import.meta.url` becomes the module's own place, and a package they load
10
+ * at run time is resolved from there too: pnpm installs cli-core's dependencies beside cli-core, not beside the CLI.
11
+ * - The build fails when a bundled module would still look in the wrong place.
12
+ */
13
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
14
+ import { isBuiltin } from "node:module";
15
+ import { basename, dirname, join, relative, sep } from "node:path";
16
+ import { pathToFileURL } from "node:url";
17
+ /** Native modules, Bun's SQLite, and what cli-core loads only by name or starts as a worker from its own file. */
18
+ export const DEFAULT_EXTERNAL = [
19
+ "bun:sqlite",
20
+ "*.node",
21
+ "better-sqlite3",
22
+ "@napi-rs/*",
23
+ "@resvg/*",
24
+ "@huggingface/*",
25
+ "sherpa-onnx*",
26
+ "onnxruntime-*",
27
+ "@wirecat/cli-core-onnx",
28
+ "@wirecat/cli-core-sqlite",
29
+ "ogg-opus-decoder",
30
+ ];
31
+ export const DEFAULT_ANCHORED = ["@wirecat/cli-core", "@wirecat/cli-base"];
32
+ // commander and @colors name these in comments and messages; liquidjs resolves partials with explicit `paths`, so the
33
+ // requiring file does not matter.
34
+ export const DEFAULT_HARMLESS = [
35
+ "/commander/lib/command.js",
36
+ "/@colors/colors/lib/colors.js",
37
+ "/liquidjs/dist/liquid.node.js",
38
+ ];
39
+ export const LOADER_MARK = "// cli-dev bundle loader\n";
40
+ const posix = (path) => path.split(sep).join("/");
41
+ const matcher = (patterns) => {
42
+ const expressions = patterns.map((pattern) => new RegExp(`^${pattern.replaceAll(".", "\\.").replaceAll("*", ".*")}$`));
43
+ return (specifier) => expressions.some((expression) => expression.test(specifier));
44
+ };
45
+ // Node has no "resolve from this parent" call; a resolve hook for the one call gives Node's own answer. Bun has no
46
+ // hooks but has `Bun.resolveSync(specifier, parent)`; when it finds nothing, the plain lookup keeps Bun's own error.
47
+ const FROM = "cli-dev-bundle:from";
48
+ const FROM_SOURCE = `import * as module from "node:module"
49
+ import { dirname } from "node:path"
50
+ import { fileURLToPath, pathToFileURL } from "node:url"
51
+ export const resolveFrom = (parent, specifier) => {
52
+ if (globalThis.Bun) {
53
+ if (specifier.includes(":")) return import.meta.resolve(specifier)
54
+ try {
55
+ return pathToFileURL(globalThis.Bun.resolveSync(specifier, dirname(fileURLToPath(parent)))).href
56
+ } catch {
57
+ return import.meta.resolve(specifier)
58
+ }
59
+ }
60
+ if (!module.registerHooks) return import.meta.resolve(specifier)
61
+ const hooks = module.registerHooks({
62
+ resolve: (asked, context, next) => next(asked, asked === specifier ? { ...context, parentURL: parent } : context),
63
+ })
64
+ try {
65
+ return import.meta.resolve(specifier)
66
+ } finally {
67
+ hooks.deregister()
68
+ }
69
+ }
70
+ export const importFrom = async (parent, specifier) => import(resolveFrom(parent, specifier))
71
+ `;
72
+ const BY_NAME = /(?<![\w$.])import\(\s*(?!["'`.])/g;
73
+ const LITERAL = /(?<![\w$.])import\(\s*(["'])([^"'.][^"']*)\1\s*\)/g;
74
+ const ownPlaces = (dist, out, anchored, harmless, isExternal, problems) => {
75
+ const placeOf = (file) => {
76
+ if (file.startsWith(dist + sep))
77
+ return `new URL(${JSON.stringify(posix(relative(out, file)))}, import.meta.url).href`;
78
+ for (const name of anchored) {
79
+ const marker = `${sep}node_modules${sep}${name.replace("/", sep)}${sep}`;
80
+ const at = file.lastIndexOf(marker);
81
+ if (at < 0)
82
+ continue;
83
+ const packageRoot = file.slice(0, at + marker.length);
84
+ const manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
85
+ const entry = manifest.exports?.["."]?.default;
86
+ if (typeof entry !== "string")
87
+ throw new Error(`${name}: package.json has no exports["."].default to anchor on`);
88
+ const path = posix(relative(dirname(join(packageRoot, entry)), file));
89
+ return `new URL(${JSON.stringify(path)}, import.meta.resolve(${JSON.stringify(name)})).href`;
90
+ }
91
+ return undefined;
92
+ };
93
+ return {
94
+ name: "own-places",
95
+ setup(context) {
96
+ context.onResolve({ filter: new RegExp(`^${FROM}$`) }, () => ({ path: FROM, namespace: FROM }));
97
+ context.onLoad({ filter: /.*/, namespace: FROM }, () => ({ contents: FROM_SOURCE, loader: "js" }));
98
+ context.onLoad({ filter: /\.[cm]?js$/ }, (args) => {
99
+ const source = readFileSync(args.path, "utf8");
100
+ const place = placeOf(args.path);
101
+ if (!place) {
102
+ const known = harmless.some((path) => posix(args.path).endsWith(path));
103
+ if (!known && /import\.meta|__dirname|__filename|require\.resolve/.test(source))
104
+ problems.push(args.path);
105
+ return undefined;
106
+ }
107
+ const loadsOutside = [...source.matchAll(LITERAL)].some(([, , specifier]) => specifier && isExternal(specifier));
108
+ if (!source.includes("import.meta") && source.search(BY_NAME) < 0 && !loadsOutside)
109
+ return undefined;
110
+ const other = source.match(/import\.meta\.(?!url\b|resolve\b)\w+/);
111
+ if (other)
112
+ problems.push(`${args.path}: ${other[0]}`);
113
+ // `place` holds `import.meta.url` or `import.meta.resolve`, so it goes in last.
114
+ const contents = source
115
+ .replaceAll(/import\.meta\.resolve\(/g, "resolveFrom(PLACE, ")
116
+ .replaceAll(BY_NAME, "importFrom(PLACE, ")
117
+ .replaceAll(LITERAL, (call, quote, specifier) => isExternal(specifier) ? `importFrom(PLACE, ${quote}${specifier}${quote})` : call)
118
+ .replaceAll(/import\.meta\.url\b/g, "PLACE")
119
+ .replaceAll(/\bPLACE\b/g, place);
120
+ return { contents: `import { importFrom, resolveFrom } from "${FROM}";\n${contents}`, loader: "js" };
121
+ });
122
+ },
123
+ };
124
+ };
125
+ const isCommonJs = (file) => {
126
+ if (file.endsWith(".cjs"))
127
+ return true;
128
+ if (!file.endsWith(".js"))
129
+ return false;
130
+ for (let directory = dirname(file); directory !== dirname(directory); directory = dirname(directory)) {
131
+ const manifest = join(directory, "package.json");
132
+ if (existsSync(manifest))
133
+ return JSON.parse(readFileSync(manifest, "utf8")).type !== "module";
134
+ }
135
+ return true;
136
+ };
137
+ /**
138
+ * `await import("yaml")` gets CommonJS's named exports from Node, but only `default` from a bundled chunk. A dynamic
139
+ * import of CommonJS goes through a module that exports the names Node itself finds in it.
140
+ */
141
+ const commonJsNames = {
142
+ name: "common-js-names",
143
+ setup(context) {
144
+ context.onResolve({ filter: /.*/ }, async (args) => {
145
+ if (args.kind !== "dynamic-import" || args.pluginData === "inner")
146
+ return undefined;
147
+ const resolved = await context.resolve(args.path, {
148
+ kind: "dynamic-import",
149
+ importer: args.importer,
150
+ resolveDir: args.resolveDir,
151
+ pluginData: "inner",
152
+ });
153
+ if (resolved.errors.length > 0 || resolved.external || !isCommonJs(resolved.path))
154
+ return undefined;
155
+ return { path: resolved.path, namespace: "common-js-names" };
156
+ });
157
+ context.onLoad({ filter: /.*/, namespace: "common-js-names" }, async (args) => {
158
+ const names = Object.keys(await import(pathToFileURL(args.path).href)).filter((name) => name !== "default");
159
+ const values = names.map((name, index) => `const n${index} = cjs[${JSON.stringify(name)}]`);
160
+ const exported = names.map((name, index) => `n${index} as ${JSON.stringify(name)}`);
161
+ return {
162
+ contents: `const cjs = require(${JSON.stringify(args.path)})\n${values.join("\n")}\nexport default cjs\nexport { ${exported.join(", ")} }\n`,
163
+ resolveDir: dirname(args.path),
164
+ loader: "js",
165
+ };
166
+ });
167
+ },
168
+ };
169
+ /** The bin that replaces the compiled one: `name` is the bundle's entry, `chunks` what a server preloads. */
170
+ export const loaderSource = (name, version, chunks) => `#!/usr/bin/env node
171
+ ${LOADER_MARK}import * as module from "node:module"
172
+
173
+ const words = process.argv.slice(2)
174
+ // As the CLI answers it, unless a test records the argv: then the CLI writes the record too.
175
+ if (
176
+ words.length === 1 &&
177
+ (words[0] === "--version" || words[0] === "-V") &&
178
+ !Object.keys(process.env).some((key) => key.endsWith("_TEST_ARGV_LOG"))
179
+ ) {
180
+ process.stdout.write(${JSON.stringify(`${version}\n`)})
181
+ } else {
182
+ // Before the bundle compiles: the bundle's own call comes after its chunks.
183
+ module.enableCompileCache()
184
+ const serves = words
185
+ .slice(0, 3)
186
+ .some((word, at) => ["serve", "watch"].includes(word) || (word === "mcp" && !/^[^-]/.test(words[at + 1] ?? "")))
187
+ if (serves && module.registerHooks) {
188
+ const bundle = new URL("../bundle/", import.meta.url).href
189
+ module.registerHooks({
190
+ resolve: (specifier, context, next) =>
191
+ specifier.startsWith("./") && context.parentURL?.startsWith(bundle)
192
+ ? { url: new URL(specifier, context.parentURL).href, format: "module", shortCircuit: true }
193
+ : next(specifier, context),
194
+ })
195
+ }
196
+ const running = import(${JSON.stringify(`../bundle/${name}.js`)})
197
+ const chunks = ${JSON.stringify(chunks)}
198
+ // After the server answers its first request, so it starts no later.
199
+ if (serves) setTimeout(() => void Promise.allSettled(chunks.map((chunk) => import(chunk))), 2000).unref()
200
+ await running
201
+ }
202
+ `;
203
+ export const bundle = async (options) => {
204
+ const { build } = await import("esbuild");
205
+ const root = options.root;
206
+ const dist = join(root, "dist");
207
+ const out = join(dist, "bundle");
208
+ const bin = join(root, options.bin);
209
+ const name = basename(bin).replace(/\.[cm]?js$/, "");
210
+ const external = [...DEFAULT_EXTERNAL, ...(options.external ?? [])];
211
+ const isExternal = matcher(external);
212
+ const problems = [];
213
+ if (!bin.startsWith(dist + sep))
214
+ return { problems: [`${options.bin} is not under dist/`], chunks: [] };
215
+ if (readFileSync(bin, "utf8").includes(LOADER_MARK))
216
+ return { problems: [`${options.bin} is already the loader — compile it again (tsc) first`], chunks: [] };
217
+ const { version } = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
218
+ rmSync(out, { recursive: true, force: true });
219
+ const { metafile } = await build({
220
+ absWorkingDir: root,
221
+ entryPoints: { [name]: bin },
222
+ outdir: out,
223
+ bundle: true,
224
+ splitting: true,
225
+ format: "esm",
226
+ platform: "node",
227
+ target: "node22",
228
+ chunkNames: "[name]-[hash]",
229
+ // Two modules with one name would otherwise become `name2`, and `name` reaches errors and `commands --json`.
230
+ keepNames: true,
231
+ external,
232
+ // Bundled CommonJS calls `require` for Node's own modules.
233
+ banner: {
234
+ js: 'import { createRequire as __bundleRequire } from "node:module";\nconst require = __bundleRequire(import.meta.url);',
235
+ },
236
+ plugins: [
237
+ commonJsNames,
238
+ ownPlaces(dist, out, [...DEFAULT_ANCHORED, ...(options.anchored ?? [])], [...DEFAULT_HARMLESS, ...(options.harmless ?? [])], isExternal, problems),
239
+ ],
240
+ metafile: true,
241
+ logLevel: "silent",
242
+ });
243
+ // Only the CLI's own modules may import a package that stays outside: it resolves from the bundle, the CLI's place.
244
+ const own = `${posix(relative(root, dist))}/`;
245
+ for (const [input, { imports }] of Object.entries(metafile.inputs)) {
246
+ if (input.startsWith(own))
247
+ continue;
248
+ for (const { path, external: outside } of imports) {
249
+ if (outside && !isBuiltin(path) && !["bun:sqlite", "<runtime>"].includes(path))
250
+ problems.push(`${input} imports ${path}, which would resolve from the bundle`);
251
+ }
252
+ }
253
+ if (problems.length > 0)
254
+ return { problems, chunks: [] };
255
+ // An upgrade replaces the chunks under a running server, and their names carry a hash: a server that had not yet
256
+ // loaded one would fail to find it. A server loads them all soon after it starts, and finds a chunk it holds without
257
+ // looking on disk, where Node's resolver would look again for each new importer.
258
+ const lazy = new Set();
259
+ for (const { imports } of Object.values(metafile.outputs)) {
260
+ for (const { path, kind, external: outside } of imports) {
261
+ if (kind === "dynamic-import" && !outside)
262
+ lazy.add(`../bundle/${basename(path)}`);
263
+ }
264
+ }
265
+ const chunks = [...lazy].sort();
266
+ const missing = chunks.filter((chunk) => !existsSync(join(out, basename(chunk))));
267
+ if (missing.length > 0)
268
+ return { problems: missing.map((chunk) => `the loader names ${chunk}, not built`), chunks };
269
+ writeFileSync(bin, loaderSource(name, version, chunks));
270
+ return { problems: [], chunks };
271
+ };
package/dist/dev/main.js CHANGED
@@ -66,7 +66,15 @@ const USAGE = `usage: cli-dev <command> [options]
66
66
  rules by its own lint/ folder; the rest by --baseline
67
67
  (audit/baseline). --groups replaces PARITY_GROUPS with the
68
68
  JSON list of parity groups in that file; --differences adds
69
- to the groups in use`;
69
+ to the groups in use
70
+ bundle --bin <dist/bin/name.js> [--external <package>]... [--anchor <package>]... [--harmless <path>]...
71
+ bundles the compiled bin with esbuild (a devDependency) into
72
+ dist/bundle/ and turns the bin into its loader. --external keeps
73
+ a package outside (* matches anything); --anchor adds a package
74
+ whose modules keep their installed place, as cli-core's do;
75
+ --harmless names a third-party file whose import.meta or
76
+ __dirname was read and found harmless. Fails when a bundled
77
+ module would look for a file or package in the wrong place`;
70
78
  const load = async (cwd, path) => (await import(pathToFileURL(isAbsolute(path) ? path : join(cwd, path)).href));
71
79
  const report = (io, problems, ok) => {
72
80
  for (const problem of problems)
@@ -604,6 +612,43 @@ const auditRun = (args, io) => {
604
612
  io.err(problem);
605
613
  return problems.length > 0 ? 1 : 0;
606
614
  };
615
+ const bundleCommand = async (args, io) => {
616
+ const { values } = parseArgs({
617
+ args,
618
+ options: {
619
+ bin: { type: "string" },
620
+ external: { type: "string", multiple: true },
621
+ anchor: { type: "string", multiple: true },
622
+ harmless: { type: "string", multiple: true },
623
+ },
624
+ });
625
+ if (!values.bin) {
626
+ io.err("bundle needs --bin <dist/bin/name.js>");
627
+ return 2;
628
+ }
629
+ // esbuild is an optional peer: the other commands, and a CLI that does not bundle, never load it.
630
+ try {
631
+ await import("esbuild");
632
+ }
633
+ catch {
634
+ io.err("cli-dev bundle needs esbuild: add it as a devDependency, in cli-base's peer range");
635
+ return 1;
636
+ }
637
+ const { bundle } = await import("./bundle.js");
638
+ const result = await bundle({
639
+ root: io.cwd,
640
+ bin: values.bin,
641
+ external: values.external ?? [],
642
+ anchored: values.anchor ?? [],
643
+ harmless: values.harmless ?? [],
644
+ });
645
+ if (result.problems.length > 0) {
646
+ io.err(`the bundle would look for these in the wrong place, or is incomplete:\n${result.problems.join("\n")}`);
647
+ return 1;
648
+ }
649
+ io.out(`bundle: ${values.bin} loads dist/bundle/; a server preloads ${result.chunks.length} chunks`);
650
+ return 0;
651
+ };
607
652
  export const main = async (argv, io) => {
608
653
  const [command, ...args] = argv;
609
654
  if (args.includes("--help")) {
@@ -649,6 +694,8 @@ export const main = async (argv, io) => {
649
694
  return lint(args, io);
650
695
  case "audit":
651
696
  return audit(args, io);
697
+ case "bundle":
698
+ return bundleCommand(args, io);
652
699
  case "help":
653
700
  case "--help":
654
701
  case "-h":
@@ -11,7 +11,7 @@ export interface SkillApp {
11
11
  }
12
12
  export declare const SKILL_TARGETS: readonly ["claude", "agents"];
13
13
  export type SkillTarget = (typeof SKILL_TARGETS)[number];
14
- /** `~/.claude/skills` is Claude Code's; `~/.agents/skills` is read by Codex and Gemini CLI. */
14
+ /** `~/.claude/skills` is Claude Code's; `~/.agents/skills` is read by Codex, Gemini CLI and OpenClaw. */
15
15
  export declare const skillPath: (target: SkillTarget, appName: string, env: Env) => string;
16
16
  /**
17
17
  * `metadata.version` from the frontmatter — where the Agent Skills specification keeps fields of its
@@ -16,7 +16,7 @@ import { executableEnvironment, executableOnPath } from "../executable.js";
16
16
  import { CHECK_EVERY_MS, isNewer, readNoticeState, writeUpdateState } from "../update/index.js";
17
17
  export const SKILL_TARGETS = ["claude", "agents"];
18
18
  const homeOf = (env) => env.HOME || env.USERPROFILE || homedir();
19
- /** `~/.claude/skills` is Claude Code's; `~/.agents/skills` is read by Codex and Gemini CLI. */
19
+ /** `~/.claude/skills` is Claude Code's; `~/.agents/skills` is read by Codex, Gemini CLI and OpenClaw. */
20
20
  export const skillPath = (target, appName, env) => join(homeOf(env), target === "claude" ? ".claude" : ".agents", "skills", appName, "SKILL.md");
21
21
  const FRONTMATTER = /^---(\r?\n)([\s\S]*?\r?\n)?---(\r?\n|$)/;
22
22
  const scalar = (key, indent) => new RegExp(`^${indent}${key}:[ \\t]*["']?([^"'\\r\\n]*?)["']?[ \\t]*\\r?\\n?$`);
@@ -234,7 +234,7 @@ export const skillCommand = (app, skill, environment, more = {}) => {
234
234
  const named = Object.keys(more);
235
235
  const show = annotate(command.command("show"), { effects: { remote: "none", local: "read" } })
236
236
  .summary("print the tool's instructions for AI agents")
237
- .description(`print SKILL.md — \`${app.command} skill install\` puts it where Claude Code, Codex and Gemini CLI look for it; --for prints the copy already there`);
237
+ .description(`print SKILL.md — \`${app.command} skill install\` puts it where Claude Code, Codex, Gemini CLI and OpenClaw look for it, and where Hermes does on request; --for prints the copy already there`);
238
238
  if (named.length > 0)
239
239
  show.argument("[name]", `one of the skills shipped for a task: ${named.join(", ")}`);
240
240
  show.addOption(new Option("--for <agents>", "print the copy installed for these agents instead of this version's").choices(SKILL_TARGETS));
@@ -64,7 +64,7 @@ export const PARITY_GROUPS = defineParityGroups([
64
64
  { name: "messengers", members: ["tg", "max"], covers: "tree", differences: [] },
65
65
  {
66
66
  name: "all",
67
- members: ["tg", "max", "memo", "drive", "zm", "wa"],
67
+ members: ["tg", "max", "memo", "drive", "zm", "wa", "gcal"],
68
68
  covers: ["commands", "doctor", "skill", "mcp", "upgrade", "search", "notes", "meetings"],
69
69
  differences: [],
70
70
  },
@@ -0,0 +1,2 @@
1
+ import type { VocabularyFlag } from "./vocabulary.js";
2
+ export declare const CALENDAR_FLAGS: VocabularyFlag[];
@@ -0,0 +1,59 @@
1
+ export const CALENDAR_FLAGS = [
2
+ { name: "--end-time", value: "<time>", role: "value", type: "time", meaning: "when it ends" },
3
+ {
4
+ name: "--start-date",
5
+ value: "<date>",
6
+ role: "value",
7
+ type: "time",
8
+ meaning: "the first day of something that lasts whole days, such as an all-day event",
9
+ },
10
+ {
11
+ name: "--end-date",
12
+ value: "<date>",
13
+ role: "value",
14
+ type: "time",
15
+ meaning: "the last day of something that lasts whole days, such as an all-day event",
16
+ },
17
+ { name: "--location", value: "<text>", role: "value", type: "string", meaning: "where it takes place" },
18
+ {
19
+ name: "--attendee",
20
+ value: "<email>",
21
+ role: "value",
22
+ type: "string",
23
+ meaning: "a person invited to the event, by email address; repeat it for more",
24
+ },
25
+ {
26
+ name: "--id",
27
+ value: "<id>",
28
+ role: "value",
29
+ type: "id",
30
+ meaning: "the id to create it with, chosen beforehand, so a retry finds it instead of making a second one",
31
+ },
32
+ {
33
+ name: "--calendar",
34
+ value: "<id>",
35
+ role: "target",
36
+ type: "id",
37
+ meaning: "the calendar it acts on, when the thing is on more than one",
38
+ },
39
+ {
40
+ name: "--repeat",
41
+ value: "<rule>",
42
+ role: "value",
43
+ type: "string",
44
+ meaning: "how it repeats, as an RFC 5545 rule such as FREQ=WEEKLY;BYDAY=MO;COUNT=10",
45
+ },
46
+ {
47
+ name: "--hours",
48
+ value: "<from-to>",
49
+ role: "target",
50
+ type: "string",
51
+ meaning: "the working hours of each day to look in, such as 09:00-17:00, in the owner's time zone",
52
+ },
53
+ {
54
+ name: "--series",
55
+ role: "target",
56
+ type: "switch",
57
+ meaning: "act on the whole repeating event, not only this occurrence",
58
+ },
59
+ ];
@@ -308,7 +308,13 @@ export const HOW_FLAGS = [
308
308
  meaning: "do not keep this run, whatever the configuration says",
309
309
  },
310
310
  { name: "--no-registries", role: "mode", type: "switch", meaning: "do not ask the public ban lists" },
311
- { name: "--notify", role: "mode", type: "switch", meaning: "tell the chat's members about it" },
311
+ {
312
+ name: "--notify",
313
+ role: "mode",
314
+ type: "switch",
315
+ meaning: "tell the people it concerns about it: the chat's members, or an event's attendees",
316
+ },
317
+ { name: "--no-notify", role: "mode", type: "switch", meaning: "tell nobody about it" },
312
318
  { name: "--offline", role: "mode", type: "switch", meaning: "answer from what was recorded and never connect" },
313
319
  {
314
320
  name: "--operation-id",
@@ -1,4 +1,5 @@
1
1
  import * as v from "valibot";
2
+ import { CALENDAR_FLAGS } from "./flags-calendar.js";
2
3
  import { HOW_FLAGS } from "./flags-how.js";
3
4
  import { WHAT_FLAGS } from "./flags-what.js";
4
5
  import { duplicates, parseRegistry, text, word } from "./registry.js";
@@ -123,6 +124,7 @@ const verbs = [
123
124
  "bring the local store up to date from the source: only what changed, or the whole list where the source cannot say; a mirror syncs both ways",
124
125
  ],
125
126
  ["fetch", "one bounded pull of one named thing into the store, not a catch-up"],
127
+ ["unsync", "stop syncing a thing; what the store already holds stays"],
126
128
  ["export", "write from the store to a file"],
127
129
  ["import", "bring a file or list the user gives into the store or the messenger, once"],
128
130
  ["download", "save a file from the source to this machine"],
@@ -153,6 +155,7 @@ const verbs = [
153
155
  ["unstar", "take the owner's star off a message"],
154
156
  ["vote", "answer a poll"],
155
157
  ["answer", "reply to a button someone pressed under a bot's message"],
158
+ ["respond", "answer an invitation: accept, tentatively accept or decline it"],
156
159
  ["press", "press a button under a bot's message; the bot sees who pressed it"],
157
160
  ["action", "show what a bot is doing in a chat, such as typing, for a few seconds"],
158
161
  ["watch", "follow new events as they come, and keep them"],
@@ -175,6 +178,8 @@ const verbs = [
175
178
  ["acknowledge", "record successful delivery using the exact receipt issued when the item was claimed"],
176
179
  ["confirm", "the owner accepts what an agent proposed"],
177
180
  ["reject", "the owner refuses what an agent proposed"],
181
+ ["review", "list what waits for the owner's decision"],
182
+ ["resolve", "settle one thing that waits for the owner's decision, with the owner's choice"],
178
183
  ["move", "move a file in the source"],
179
184
  ["copy", "copy a file in the source"],
180
185
  ["run", "run a saved job once, now, as its timer would"],
@@ -241,6 +246,9 @@ const collections = [
241
246
  ["files", "files in the source, such as Google Drive, read and changed there"],
242
247
  ["comments", "comments people left on a thing in the source: a channel post or a file"],
243
248
  ["meetings", "stored meetings, from Zoom or another provider"],
249
+ ["calendars", "calendars of an account, each synced or not"],
250
+ ["events", "calendar entries: meetings, appointments and other things on a calendar"],
251
+ ["people", "persons the owner deals with, such as the attendees of events"],
244
252
  ["recordings", "the files a provider recorded of a meeting"],
245
253
  ["jobs", "work this tool runs in the background or on a timer"],
246
254
  ["actions", "what an agent proposes to do; the owner confirms or rejects each"],
@@ -302,6 +310,7 @@ const views = [
302
310
  ["evidence", "the stored messages or lines a result or a reference rests on"],
303
311
  ["passage", "a cited transcript line with bounded surrounding lines from the same revision"],
304
312
  ["agenda", "what needs a look today"],
313
+ ["busy", "when calendars or people are taken, without what takes them"],
305
314
  ["categories", "the categories a thing takes, with what each means"],
306
315
  ["domains", "the internet domains links point to, most linked first"],
307
316
  ["places", "the street addresses confirmed as places, most mentioned first"],
@@ -348,5 +357,5 @@ export const VOCABULARY = defineVocabulary({
348
357
  ],
349
358
  views: views.map(([entry, meaning]) => ({ word: entry, meaning })),
350
359
  roots: roots.map(([entry, meaning]) => ({ word: entry, meaning })),
351
- flags: [...HOW_FLAGS, ...WHAT_FLAGS],
360
+ flags: [...HOW_FLAGS, ...WHAT_FLAGS, ...CALENDAR_FLAGS],
352
361
  });
@@ -28,12 +28,12 @@ the four steps that need a person. A new CLI is ready for its first release when
28
28
  | Page | Area id | Rules | What it covers |
29
29
  |---|---|---:|---|
30
30
  | [Toolchain](toolchain.md) | `TOOL` | 20 | TypeScript, Biome, grit plugins, size caps, import cycles, spelling, Markdown, Go |
31
- | [Layout](layout.md) | `LAY` | 9 | Repository layout, entry points, exports, file size, one job per file, splitting a unit |
31
+ | [Layout](layout.md) | `LAY` | 10 | Repository layout, entry points, exports, file size, one job per file, splitting a unit, the bundled bin |
32
32
  | [Commands](commands.md) | `CMD` | 28 | Paths, vocabulary, flags, arguments, effects, lifetime, query, help, external references |
33
33
  | [Output](output.md) | `OUT` | 23 | Data on stdout only, envelopes, errors, exit codes, partial answers, projection |
34
34
  | [MCP](mcp.md) | `MCP` | 13 | The MCP tools, how they come from commands, what each permission shows |
35
35
  | [Accounts](accounts.md) | `ACCT` | 11 | The explicit profile, reads across accounts, scope flags, linked identities |
36
- | [Permissions](permissions.md) | `PERM` | 21 | Levels, keys, defaults, hard limits, the write guard and its journal |
36
+ | [Permissions](permissions.md) | `PERM` | 22 | Levels, keys, defaults, hard limits, the write guard and its journal |
37
37
  | [Architecture](architecture.md) | `ARCH` | 17 | Layers, dependency direction, vendor types, what goes in which package |
38
38
  | [Lifecycle](lifecycle.md) | `LIFE` | 14 | Opening late, who closes, one-shot exits, long-running commands, the `serve` relay, resumable syncs, the run log |
39
39
  | [Store](store.md) | `STORE` | 14 | Store areas, account scope, migrations, test isolation |
@@ -84,7 +84,7 @@ Every rule has the same shape. A rule may add a sentence or a short list under i
84
84
  CLI's `node_modules`;
85
85
  - a check in another repository, as `<repo>: <check>`, such as `cli-core: pnpm parity:check` or
86
86
  `cli-core: biome noRestrictedImports`. `<repo>` is the repository's name on GitHub: `cli-core`, `cli-testing`,
87
- `cli-docs`, `community`, `tg-cli`, `max-cli`, `cli-memo`, `drive-cli`, `zoom-cli` or `whatsapp-cli`. `<app>`
87
+ `cli-docs`, `community`, `tg-cli`, `max-cli`, `cli-memo`, `drive-cli`, `zoom-cli`, `whatsapp-cli` or `gcal-cli`. `<app>`
88
88
  stands for each CLI the rule applies to, when every one of them runs that check in its own repository.
89
89
  - **Since** is the cli-base release whose handbook first carried the rule. A feature can be older than its rule.
90
90
 
@@ -57,8 +57,8 @@ CLI with a `setup` command suggests it, and any other CLI suggests the command t
57
57
  - **Enforced by:** `cli-core: src/cli/settings.test.ts` · `cli-core: src/cli/program.test.ts`.
58
58
  - **Example:** ``name a profile: `tg <profile> …` — profiles here: personal, work (or set TG_PROFILE)``. With none,
59
59
  tg, which has `setup`, says to ``create one with `tg personal setup` (any name in place of personal)``. A CLI
60
- without `setup`, such as one made from cli-template, answers `cal events list` with ``name a profile: there is none
61
- on this computer yet — any name works, such as `cal personal events list` (or set CAL_PROFILE)`` (cli-core 0.424.0
60
+ without `setup`, such as one made from cli-template, answers `gcal events list` with ``name a profile: there is none
61
+ on this computer yet — any name works, such as `gcal personal events list` (or set GCAL_PROFILE)`` (cli-core 0.424.0
62
62
  and later).
63
63
  - **Exceptions:** none.
64
64
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.