@wirecat/cli-base 0.63.0 → 0.65.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +232 -0
  2. package/dist/codegen/discovery.d.ts +3 -3
  3. package/dist/codegen/discovery.js +18 -9
  4. package/dist/codegen/index.d.ts +3 -1
  5. package/dist/codegen/index.js +3 -1
  6. package/dist/codegen/json-schema.d.ts +8 -0
  7. package/dist/codegen/json-schema.js +78 -0
  8. package/dist/commands/index.d.ts +1 -1
  9. package/dist/commands/index.js +1 -1
  10. package/dist/credentials.d.ts +1 -1
  11. package/dist/dev/bundle.d.ts +31 -0
  12. package/dist/dev/bundle.js +280 -0
  13. package/dist/dev/main.js +48 -1
  14. package/dist/skill/index.d.ts +1 -1
  15. package/dist/skill/index.js +2 -2
  16. package/dist/standards/audit.js +8 -0
  17. package/dist/standards/flags-calendar.d.ts +2 -0
  18. package/dist/standards/flags-calendar.js +72 -0
  19. package/dist/standards/flags-how.js +7 -1
  20. package/dist/standards/flags-what.js +7 -0
  21. package/dist/standards/vocabulary.js +13 -2
  22. package/docs/standards/README.md +5 -5
  23. package/docs/standards/accounts.md +44 -11
  24. package/docs/standards/architecture.md +19 -12
  25. package/docs/standards/ci.md +33 -12
  26. package/docs/standards/commands.md +35 -10
  27. package/docs/standards/data-model.md +5 -1
  28. package/docs/standards/docs.md +4 -1
  29. package/docs/standards/kit-surface.md +6 -5
  30. package/docs/standards/layout.md +32 -0
  31. package/docs/standards/lifecycle.md +69 -8
  32. package/docs/standards/mcp.md +48 -24
  33. package/docs/standards/new-cli.md +30 -16
  34. package/docs/standards/output.md +13 -2
  35. package/docs/standards/permissions.md +46 -3
  36. package/docs/standards/releases.md +21 -5
  37. package/docs/standards/security.md +16 -8
  38. package/docs/standards/store.md +4 -3
  39. package/docs/standards/testing.md +19 -8
  40. package/docs/standards/time.md +8 -1
  41. package/docs/standards/toolchain.md +24 -18
  42. package/package.json +6 -1
@@ -0,0 +1,280 @@
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
+ /**
170
+ * Whether the loader runs a server: `serve`, `watch` or `mcp serve`, or `mcp` alone as before cli-core 0.463. Flags
171
+ * are skipped, so `<cli> personal --read-only mcp serve` counts; four words leave room for one flag's value. It is
172
+ * written into the loader as source, so it uses nothing from outside.
173
+ */
174
+ export const isServer = (words) => {
175
+ const plain = words.filter((word) => !word.startsWith("-"));
176
+ return plain
177
+ .slice(0, 4)
178
+ .some((word, at) => word === "serve" || word === "watch" || (word === "mcp" && plain[at + 1] === undefined));
179
+ };
180
+ /** The bin that replaces the compiled one: `name` is the bundle's entry, `chunks` what a server preloads. */
181
+ export const loaderSource = (name, version, chunks) => `#!/usr/bin/env node
182
+ ${LOADER_MARK}import * as module from "node:module"
183
+
184
+ const words = process.argv.slice(2)
185
+ // As the CLI answers it, unless a test records the argv: then the CLI writes the record too.
186
+ if (
187
+ words.length === 1 &&
188
+ (words[0] === "--version" || words[0] === "-V") &&
189
+ !Object.keys(process.env).some((key) => key.endsWith("_TEST_ARGV_LOG"))
190
+ ) {
191
+ process.stdout.write(${JSON.stringify(`${version}\n`)})
192
+ } else {
193
+ // Before the bundle compiles: the bundle's own call comes after its chunks.
194
+ module.enableCompileCache()
195
+ const serves = (${isServer.toString()})(words)
196
+ if (serves && module.registerHooks) {
197
+ const bundle = new URL("../bundle/", import.meta.url).href
198
+ module.registerHooks({
199
+ resolve: (specifier, context, next) =>
200
+ specifier.startsWith("./") && context.parentURL?.startsWith(bundle)
201
+ ? { url: new URL(specifier, context.parentURL).href, format: "module", shortCircuit: true }
202
+ : next(specifier, context),
203
+ })
204
+ }
205
+ const running = import(${JSON.stringify(`../bundle/${name}.js`)})
206
+ const chunks = ${JSON.stringify(chunks)}
207
+ // After the server answers its first request, so it starts no later.
208
+ if (serves) setTimeout(() => void Promise.allSettled(chunks.map((chunk) => import(chunk))), 2000).unref()
209
+ await running
210
+ }
211
+ `;
212
+ export const bundle = async (options) => {
213
+ const { build } = await import("esbuild");
214
+ const root = options.root;
215
+ const dist = join(root, "dist");
216
+ const out = join(dist, "bundle");
217
+ const bin = join(root, options.bin);
218
+ const name = basename(bin).replace(/\.[cm]?js$/, "");
219
+ const external = [...DEFAULT_EXTERNAL, ...(options.external ?? [])];
220
+ const isExternal = matcher(external);
221
+ const problems = [];
222
+ if (!bin.startsWith(dist + sep))
223
+ return { problems: [`${options.bin} is not under dist/`], chunks: [] };
224
+ if (readFileSync(bin, "utf8").includes(LOADER_MARK))
225
+ return { problems: [`${options.bin} is already the loader — compile it again (tsc) first`], chunks: [] };
226
+ const { version } = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
227
+ rmSync(out, { recursive: true, force: true });
228
+ const { metafile } = await build({
229
+ absWorkingDir: root,
230
+ entryPoints: { [name]: bin },
231
+ outdir: out,
232
+ bundle: true,
233
+ splitting: true,
234
+ format: "esm",
235
+ platform: "node",
236
+ target: "node22",
237
+ chunkNames: "[name]-[hash]",
238
+ // Two modules with one name would otherwise become `name2`, and `name` reaches errors and `commands --json`.
239
+ keepNames: true,
240
+ external,
241
+ // Bundled CommonJS calls `require` for Node's own modules.
242
+ banner: {
243
+ js: 'import { createRequire as __bundleRequire } from "node:module";\nconst require = __bundleRequire(import.meta.url);',
244
+ },
245
+ plugins: [
246
+ commonJsNames,
247
+ ownPlaces(dist, out, [...DEFAULT_ANCHORED, ...(options.anchored ?? [])], [...DEFAULT_HARMLESS, ...(options.harmless ?? [])], isExternal, problems),
248
+ ],
249
+ metafile: true,
250
+ logLevel: "silent",
251
+ });
252
+ // Only the CLI's own modules may import a package that stays outside: it resolves from the bundle, the CLI's place.
253
+ const own = `${posix(relative(root, dist))}/`;
254
+ for (const [input, { imports }] of Object.entries(metafile.inputs)) {
255
+ if (input.startsWith(own))
256
+ continue;
257
+ for (const { path, external: outside } of imports) {
258
+ if (outside && !isBuiltin(path) && !["bun:sqlite", "<runtime>"].includes(path))
259
+ problems.push(`${input} imports ${path}, which would resolve from the bundle`);
260
+ }
261
+ }
262
+ if (problems.length > 0)
263
+ return { problems, chunks: [] };
264
+ // An upgrade replaces the chunks under a running server, and their names carry a hash: a server that had not yet
265
+ // loaded one would fail to find it. A server loads them all soon after it starts, and finds a chunk it holds without
266
+ // looking on disk, where Node's resolver would look again for each new importer.
267
+ const lazy = new Set();
268
+ for (const { imports } of Object.values(metafile.outputs)) {
269
+ for (const { path, kind, external: outside } of imports) {
270
+ if (kind === "dynamic-import" && !outside)
271
+ lazy.add(`../bundle/${basename(path)}`);
272
+ }
273
+ }
274
+ const chunks = [...lazy].sort();
275
+ const missing = chunks.filter((chunk) => !existsSync(join(out, basename(chunk))));
276
+ if (missing.length > 0)
277
+ return { problems: missing.map((chunk) => `the loader names ${chunk}, not built`), chunks };
278
+ writeFileSync(bin, loaderSource(name, version, chunks));
279
+ return { problems: [], chunks };
280
+ };
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));
@@ -68,6 +68,14 @@ export const PARITY_GROUPS = defineParityGroups([
68
68
  covers: ["commands", "doctor", "skill", "mcp", "upgrade", "search", "notes", "meetings"],
69
69
  differences: [],
70
70
  },
71
+ // gcal is compared on what it already shares with every CLI; doctor, mcp, search and skill wait until the published
72
+ // CLIs agree there (https://github.com/WireCatLabs/cli-base/issues/390), then gcal moves back into "all".
73
+ {
74
+ name: "gcal-shared",
75
+ members: ["tg", "max", "memo", "drive", "zm", "wa", "gcal"],
76
+ covers: ["commands", "upgrade", "notes", "meetings"],
77
+ differences: [],
78
+ },
71
79
  ]);
72
80
  const requiredRootSchema = v.strictObject({ path: v.pipe(v.array(segment), v.minLength(1)), meaning: text });
73
81
  export const defineRequiredRoots = (input) => parseRegistry("required roots", v.array(requiredRootSchema), input, (roots) => duplicates(roots.map((root) => root.path.join(" "))).map((path) => `"${path}" is listed twice`));
@@ -0,0 +1,2 @@
1
+ import type { VocabularyFlag } from "./vocabulary.js";
2
+ export declare const CALENDAR_FLAGS: VocabularyFlag[];
@@ -0,0 +1,72 @@
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
+ {
60
+ name: "--following",
61
+ role: "target",
62
+ type: "switch",
63
+ meaning: "act on this occurrence and every later one of the repeating event",
64
+ },
65
+ {
66
+ name: "--to-calendar",
67
+ value: "<id>",
68
+ role: "target",
69
+ type: "id",
70
+ meaning: "the calendar it moves to, of the same account",
71
+ },
72
+ ];
@@ -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",
@@ -187,6 +187,13 @@ export const WHAT_FLAGS = [
187
187
  type: "string",
188
188
  meaning: "the profile to use, instead of the first word",
189
189
  },
190
+ {
191
+ name: "--profiles",
192
+ value: "<names|all>",
193
+ role: "scope",
194
+ type: "list",
195
+ meaning: "the profiles an MCP server serves, comma-separated, or all of them",
196
+ },
190
197
  {
191
198
  name: "--history-since-time",
192
199
  value: "<time>",
@@ -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";
@@ -114,7 +115,9 @@ const verbs = [
114
115
  ["start", "start a running process, or a login session"],
115
116
  ["stop", "stop a running process"],
116
117
  ["restart", "stop and start a running process"],
118
+ ["serve", "run a server in this process and answer its clients until stopped"],
117
119
  ["end", "end a login session"],
120
+ ["revoke", "take back what was granted, so it stops working: a login, an invite link, a consent"],
118
121
  ["install", "install a system unit"],
119
122
  ["uninstall", "remove a system unit"],
120
123
  ["cancel", "stop a job or a pending thing"],
@@ -123,6 +126,7 @@ const verbs = [
123
126
  "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
127
  ],
125
128
  ["fetch", "one bounded pull of one named thing into the store, not a catch-up"],
129
+ ["unsync", "stop syncing a thing; what the store already holds stays"],
126
130
  ["export", "write from the store to a file"],
127
131
  ["import", "bring a file or list the user gives into the store or the messenger, once"],
128
132
  ["download", "save a file from the source to this machine"],
@@ -153,6 +157,7 @@ const verbs = [
153
157
  ["unstar", "take the owner's star off a message"],
154
158
  ["vote", "answer a poll"],
155
159
  ["answer", "reply to a button someone pressed under a bot's message"],
160
+ ["respond", "answer an invitation: accept, tentatively accept or decline it"],
156
161
  ["press", "press a button under a bot's message; the bot sees who pressed it"],
157
162
  ["action", "show what a bot is doing in a chat, such as typing, for a few seconds"],
158
163
  ["watch", "follow new events as they come, and keep them"],
@@ -175,7 +180,9 @@ const verbs = [
175
180
  ["acknowledge", "record successful delivery using the exact receipt issued when the item was claimed"],
176
181
  ["confirm", "the owner accepts what an agent proposed"],
177
182
  ["reject", "the owner refuses what an agent proposed"],
178
- ["move", "move a file in the source"],
183
+ ["review", "list what waits for the owner's decision"],
184
+ ["resolve", "settle one thing that waits for the owner's decision, with the owner's choice"],
185
+ ["move", "move a thing to another place in the source: a file to a folder, an event to a calendar"],
179
186
  ["copy", "copy a file in the source"],
180
187
  ["run", "run a saved job once, now, as its timer would"],
181
188
  ["claim", "take a thing for a while so no other agent works it at once; the claim lapses by itself"],
@@ -241,6 +248,9 @@ const collections = [
241
248
  ["files", "files in the source, such as Google Drive, read and changed there"],
242
249
  ["comments", "comments people left on a thing in the source: a channel post or a file"],
243
250
  ["meetings", "stored meetings, from Zoom or another provider"],
251
+ ["calendars", "calendars of an account, each synced or not"],
252
+ ["events", "calendar entries: meetings, appointments and other things on a calendar"],
253
+ ["people", "persons the owner deals with, such as the attendees of events"],
244
254
  ["recordings", "the files a provider recorded of a meeting"],
245
255
  ["jobs", "work this tool runs in the background or on a timer"],
246
256
  ["actions", "what an agent proposes to do; the owner confirms or rejects each"],
@@ -302,6 +312,7 @@ const views = [
302
312
  ["evidence", "the stored messages or lines a result or a reference rests on"],
303
313
  ["passage", "a cited transcript line with bounded surrounding lines from the same revision"],
304
314
  ["agenda", "what needs a look today"],
315
+ ["busy", "when calendars or people are taken, without what takes them"],
305
316
  ["categories", "the categories a thing takes, with what each means"],
306
317
  ["domains", "the internet domains links point to, most linked first"],
307
318
  ["places", "the street addresses confirmed as places, most mentioned first"],
@@ -348,5 +359,5 @@ export const VOCABULARY = defineVocabulary({
348
359
  ],
349
360
  views: views.map(([entry, meaning]) => ({ word: entry, meaning })),
350
361
  roots: roots.map(([entry, meaning]) => ({ word: entry, meaning })),
351
- flags: [...HOW_FLAGS, ...WHAT_FLAGS],
362
+ flags: [...HOW_FLAGS, ...WHAT_FLAGS, ...CALENDAR_FLAGS],
352
363
  });
@@ -28,14 +28,14 @@ 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
- | [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 |
35
+ | [Accounts](accounts.md) | `ACCT` | 12 | The explicit profile, reads across accounts, scope flags, linked identities, sign-in commands |
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
- | [Lifecycle](lifecycle.md) | `LIFE` | 14 | Opening late, who closes, one-shot exits, long-running commands, the `serve` relay, resumable syncs, the run log |
38
+ | [Lifecycle](lifecycle.md) | `LIFE` | 15 | Opening late, who closes, deadlines, long-running commands, the `serve` relay, resumable syncs and their request budget, the run log |
39
39
  | [Store](store.md) | `STORE` | 14 | Store areas, account scope, migrations, test isolation |
40
40
  | [Data model](data-model.md) | `DATA` | 9 | Core entities, entities and source records, `external_id`, field names, links and duplicates, files, calendar entries |
41
41
  | [Testing](testing.md) | `TEST` | 17 | Sandbox, no network, no real waits, leak tests, coverage, contracts, test matrix, parity |
@@ -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