burgee 0.11.0 → 0.11.1

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/README.md CHANGED
@@ -46,6 +46,7 @@ run(defineCommand({
46
46
  name: 'greet',
47
47
  description: 'Greet someone by name',
48
48
  options: { name: { type: 'string', required: true, description: 'who to greet' } },
49
+ effects: 'read_only', // what running it does to the world; required, and what --mcp reads
49
50
  run: ({ options }) => ({ greeting: `hello, ${options.name}` }),
50
51
  }));
51
52
  ```
@@ -55,7 +56,7 @@ $ node cli.mjs --name ada
55
56
  greeting: hello, ada
56
57
 
57
58
  $ node cli.mjs --json --name ada
58
- {"ok":true,"data":{"greeting":"hello, ada"}}
59
+ {"ok":true,"data":{"greeting":"hello, ada"},"meta":{"provenance":{"name":{"source":"flag","location":"--name"}}}}
59
60
 
60
61
  $ node cli.mjs # exit 2
61
62
  error: missing required option --name
@@ -154,9 +155,11 @@ the declaration read by a different reader.
154
155
 
155
156
  ### How do I expose a CLI over MCP?
156
157
 
157
- Run it with `--mcp`: the same manifest is served as MCP tools over stdio. A command becomes
158
- a tool only when it declares its `effects` (`read_only`, `idempotent` or `non_idempotent`),
159
- so nothing reaches an agent by accident. Register it with any stdio client:
158
+ Run it with `--mcp`: the same manifest is served as MCP tools over stdio. Every runnable
159
+ command declares its `effects` — `read_only`, `idempotent` or `non_idempotent`, which become
160
+ MCP's hints, or `withheld`, which keeps it out of the tool list — so nothing reaches an
161
+ agent by accident. On `burgee/commander` and `burgee/yargs`, a command that declared
162
+ nothing is still listed, marked `effects: 'undeclared'`. Register it with any stdio client:
160
163
 
161
164
  ```json
162
165
  { "mcpServers": { "mytool": { "command": "npx", "args": ["mytool", "--mcp"] } } }
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  export declare const brandCommand: import("./execute.js").Command<{
2
3
  readonly lead: {
3
4
  readonly type: "string";
package/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { DEFAULT_GROUND, defineBurgee, opposedField } from './brand.js';
@@ -329,7 +329,17 @@ export declare class Command extends EventEmitter {
329
329
  */
330
330
  get manifest(): Manifest;
331
331
  _project(manifest: Manifest): void;
332
- /** Options as the manifest describes them, on a null-prototype record. */
332
+ /**
333
+ * Options as the manifest describes them, on a null-prototype record — keyed so that the
334
+ * spelling every surface derives from a key, `--${kebab(key)}`, is a flag commander accepts.
335
+ *
336
+ * Commander negates only what it was told to, where burgee's own parser negates every
337
+ * boolean. So a boolean is `negatable` here only when the program declared its `--no-` twin,
338
+ * which folds onto it rather than appearing twice; and a `--no-x` declared alone, which has
339
+ * no `--x`, is described as the switch it is, under `noX`. Before this, `--no-color` was
340
+ * published as `color` with `flag: '--color'`: completions offered `--color` and
341
+ * `--no-skip-blank`, and `--mcp` sent `--color` — each refused by the parser behind them.
342
+ */
333
343
  _optionSpecs(): Record<string, OptionSpec>;
334
344
  /** burgee: declare what the command does to the world (N6). This is what exposes it as an MCP tool (N2). */
335
345
  effects(value: DeclaredEffects): this;
@@ -5,6 +5,7 @@ import { stripVTControlCharacters } from 'node:util';
5
5
  import * as crossSpawn from 'bellpull/cross-spawn';
6
6
  import { ExitCode } from '../exit-code.js';
7
7
  import { Manifest } from '../manifest.js';
8
+ import { camel } from '../names.js';
8
9
  import { host } from '../runtime.js';
9
10
  import { machineJson, schemaOf } from '../schema.js';
10
11
  import { suggestSimilar } from '../suggest.js';
@@ -1413,6 +1414,10 @@ Expecting one of '${HELP_POSITIONS.join("', '")}'`);
1413
1414
  _optionSpecs() {
1414
1415
  const specs = Object.create(null);
1415
1416
  for (const option of this.options) {
1417
+ const name = option.attributeName();
1418
+ const paired = this.options.some((o) => o.negate !== option.negate && o.attributeName() === name);
1419
+ if (option.negate && paired)
1420
+ continue;
1416
1421
  const spec = { type: option.required || option.optional ? 'string' : 'boolean' };
1417
1422
  if (option.description)
1418
1423
  spec.description = option.description;
@@ -1424,7 +1429,9 @@ Expecting one of '${HELP_POSITIONS.join("', '")}'`);
1424
1429
  spec.default = option.defaultValue;
1425
1430
  if (option.envVar)
1426
1431
  spec.env = option.envVar;
1427
- Object.defineProperty(specs, option.attributeName(), { value: spec, enumerable: true, writable: true, configurable: true });
1432
+ if (spec.type === 'boolean')
1433
+ spec.negatable = paired;
1434
+ Object.defineProperty(specs, option.negate ? camel(option.name()) : name, { value: spec, enumerable: true, writable: true, configurable: true });
1428
1435
  }
1429
1436
  return specs;
1430
1437
  }
@@ -16,7 +16,7 @@ function children(manifest, at) {
16
16
  function nodeOf(manifest, c) {
17
17
  const visible = Object.fromEntries(Object.entries(c.options)
18
18
  .filter(([, spec]) => spec.hidden !== true)
19
- .map(([name, spec]) => [name, spec.type === 'boolean' ? { ...spec, negatable: true } : spec]));
19
+ .map(([name, spec]) => [name, { ...spec, negatable: spec.type === 'boolean' && spec.negatable !== false }]));
20
20
  const isRoot = c.path.length === manifest.rootPath.length;
21
21
  return {
22
22
  path: c.path.slice(manifest.rootPath.length),
package/dist/execute.js CHANGED
@@ -127,7 +127,7 @@ function toParseConfig(specs, withConfig) {
127
127
  ...(spec.short === undefined ? {} : { short: spec.short }),
128
128
  ...(spec.multiple === true ? { multiple: true } : {}),
129
129
  };
130
- if (spec.type === 'boolean')
130
+ if (spec.type === 'boolean' && spec.negatable !== false)
131
131
  config[`${NO}${kebab(name)}`] = { type: 'boolean' };
132
132
  }
133
133
  return config;
@@ -238,7 +238,7 @@ async function describeFailure(cause, argv, node) {
238
238
  const dash = explain.singleDashHint(argv);
239
239
  if (dash !== undefined)
240
240
  return { code: ExitCode.USAGE, message, hint: dash };
241
- const better = explain.unknownOption(cause, Object.keys(node?.options ?? {}));
241
+ const better = explain.unknownOption(cause, Object.keys(node?.options ?? {}).map(kebab));
242
242
  return { code: ExitCode.USAGE, message, hint: 'run --help to see the available options', ...better };
243
243
  }
244
244
  return { code: ExitCode.RUNTIME, message };
@@ -505,7 +505,7 @@ export async function run(target, opts = {}) {
505
505
  manifest.rootPath = [target.name];
506
506
  manifest.add({
507
507
  path: [target.name],
508
- ...(target.description === undefined ? {} : { description: target.description }),
508
+ ...helpFields(target),
509
509
  options: target.options ?? {},
510
510
  ...(target.run === undefined ? {} : { run: target.run }),
511
511
  });
@@ -78,6 +78,13 @@ export interface OptionSpec {
78
78
  /** `true` renders `(deprecated)`; a string names the replacement: `(deprecated: use --force)` (yargs #2248). */
79
79
  deprecated?: boolean | string;
80
80
  hidden?: boolean;
81
+ /**
82
+ * `boolean` only: whether `--no-<name>` is accepted. Every boolean is negatable unless this
83
+ * says `false`. burgee's own parser negates every boolean (see `toParseConfig`); the
84
+ * commander façade sets `false` where commander would refuse the negation, so completions
85
+ * and `--mcp` never offer or send a flag the parser behind them rejects.
86
+ */
87
+ negatable?: boolean;
81
88
  /** The shared set this option was copied from (M4); `--schema` carries it, help lists the option like any other. */
82
89
  sharedFrom?: string;
83
90
  }
package/dist/mcp.d.ts CHANGED
@@ -34,7 +34,11 @@ export type Invoke = (argv: string[]) => Promise<{
34
34
  * this command is not a `read_only` one, and it is not a `withheld` one either — nobody said.
35
35
  */
36
36
  export declare function annotationsOf(effects?: Effects): ToolAnnotations;
37
- /** `config get` → `config_get`: MCP tool names are `[a-zA-Z0-9_-]`. */
37
+ /**
38
+ * `config get` → `config_get`: MCP tool names are `[a-zA-Z0-9_-]`, one character or more. A
39
+ * program whose root runs — a single `run(defineCommand(…))`, a commander program with a root
40
+ * `.action()` — has no typed name at all, so its tool is named after the program.
41
+ */
38
42
  export declare const toolName: (node: CommandNode, root: string[]) => string;
39
43
  /**
40
44
  * The tool list: every runnable, visible command an author has not withheld.
package/dist/mcp.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createInterface } from 'node:readline';
2
2
  import { WITHHELD } from './definition.js';
3
+ import { kebab } from './names.js';
3
4
  import { inputSchemaOf, runnable, typedName } from './schema.js';
4
5
  export const MCP_PROTOCOL_VERSION = '2025-06-18';
5
6
  const JSON_RPC_INVALID_REQUEST = -32600;
@@ -14,7 +15,7 @@ export function annotationsOf(effects) {
14
15
  destructiveHint: effects === 'non_idempotent',
15
16
  };
16
17
  }
17
- export const toolName = (node, root) => typedName(node, root).replaceAll(' ', '_');
18
+ export const toolName = (node, root) => (typedName(node, root) || root.join(' ')).replaceAll(' ', '_');
18
19
  function describe(node) {
19
20
  const parts = [node.description ?? node.summary ?? ''];
20
21
  for (const e of node.examples ?? [])
@@ -32,12 +33,13 @@ function optionArgs(node, args) {
32
33
  const value = args[name];
33
34
  if (value === undefined || value === null)
34
35
  continue;
35
- if (spec.type === 'boolean') {
36
- if (value === true)
37
- out.push(`--${name}`);
38
- }
39
- else
40
- out.push(`--${name}`, String(value));
36
+ const flag = `--${kebab(name)}`;
37
+ if (spec.type !== 'boolean')
38
+ out.push(flag, String(value));
39
+ else if (value === true)
40
+ out.push(flag);
41
+ else if (value === false && spec.negatable !== false && (spec.default === true || spec.env !== undefined))
42
+ out.push(`--no-${kebab(name)}`);
41
43
  }
42
44
  return out;
43
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "burgee",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "An agent-native CLI framework, drop-in compatible with commander and yargs. One declaration; help, --json, --schema, --mcp and completions all projected from it.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -146,11 +146,11 @@
146
146
  "provenance": true
147
147
  },
148
148
  "dependencies": {
149
- "bellpull": "^0.3.0",
150
- "closeout": "^0.5.0",
151
- "linegauge": "^0.5.0",
152
- "roundel": "^0.5.0",
153
- "seniority": "^0.5.0"
149
+ "bellpull": "^0.3.1",
150
+ "closeout": "^0.5.1",
151
+ "linegauge": "^0.5.1",
152
+ "roundel": "^0.5.1",
153
+ "seniority": "^0.5.1"
154
154
  },
155
155
  "devDependencies": {
156
156
  "vitest": "^5.0.1"