cli-agent-mode 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +136 -2
- package/dist/commander-augment.d.ts +8 -0
- package/dist/index.cjs +337 -0
- package/dist/index.d.cts +135 -0
- package/dist/index.d.mts +135 -0
- package/dist/index.mjs +330 -0
- package/package.json +67 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gordey Levchenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,137 @@
|
|
|
1
|
-
#
|
|
1
|
+
# cli-agent-mode
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/cli-agent-mode)
|
|
4
|
+
[](https://github.com/missingbits/cli-agent-mode/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**Make any [commander.js](https://github.com/tj/commander.js) CLI usable by AI agents** (Claude Code, Cursor, Codex, Gemini CLI, Copilot) **without breaking it for humans.**
|
|
8
|
+
|
|
9
|
+
Two lines turn your CLI into one that agents can drive reliably:
|
|
10
|
+
|
|
11
|
+
- **`--json` output envelope** — every command prints exactly one JSON document on stdout: `{"ok":true,"data":...}` or `{"ok":false,"error":{...}}`.
|
|
12
|
+
- **Machine-readable errors** — stable `code`, a human `message`, and a `suggestion` the agent can follow literally. Commander usage errors are mapped too (`UNKNOWN_OPTION`, `MISSING_ARGUMENT`, ...).
|
|
13
|
+
- **Self-description** — `mycli describe [command]` (and `--help --json`) print the full CLI schema in one call: arguments, option flags, defaults, choices, subcommands. Compact by design (falsy fields and empty lists are omitted) to save agent tokens.
|
|
14
|
+
- **Auto-detects agents** — JSON turns on automatically under `AI_AGENT`, `CLAUDECODE`, `CURSOR_AGENT`, `GEMINI_CLI`, `CODEX_SANDBOX`, ... (precedence: `--json` > `CLI_AGENT_MODE` > auto-detect)
|
|
15
|
+
- **Escape hatch** — `CLI_AGENT_MODE=human` forces human output even under an agent, `CLI_AGENT_MODE=json` forces JSON.
|
|
16
|
+
- **Stable exit codes** — `0` ok, `1` runtime error, `2` usage error.
|
|
17
|
+
- **Clean stdout** — stray `console.log` inside actions is redirected to stderr in JSON mode.
|
|
18
|
+
|
|
19
|
+
Zero dependencies. ~4 kB gzipped. ESM and CommonJS, Node 20+, commander 12+.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm i cli-agent-mode
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { Command } from 'commander';
|
|
29
|
+
import { AgentError, agentMode, run } from 'cli-agent-mode';
|
|
30
|
+
|
|
31
|
+
const program = agentMode(new Command('deployctl')).version('1.0.0'); // 1. wrap the program
|
|
32
|
+
|
|
33
|
+
program
|
|
34
|
+
.command('deploy')
|
|
35
|
+
.argument('<env>', 'environment name')
|
|
36
|
+
.option('--dry-run', 'show the plan without deploying')
|
|
37
|
+
.action(async (env, opts) => {
|
|
38
|
+
if (!['staging', 'prod'].includes(env)) {
|
|
39
|
+
throw new AgentError('ENV_NOT_FOUND', `Unknown environment "${env}"`, {
|
|
40
|
+
suggestion: 'Use one of: staging, prod.',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return { env, url: `https://${env}.example.com` }; // return data instead of printing it
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await run(program); // 2. replace program.parseAsync()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
What an agent sees:
|
|
50
|
+
|
|
51
|
+
```console
|
|
52
|
+
$ deployctl deploy prod --json
|
|
53
|
+
{"ok":true,"data":{"env":"prod","url":"https://prod.example.com"}}
|
|
54
|
+
|
|
55
|
+
$ deployctl deploy mars --json # exit code 1
|
|
56
|
+
{"ok":false,"error":{"code":"ENV_NOT_FOUND","message":"Unknown environment \"mars\"","suggestion":"Use one of: staging, prod."}}
|
|
57
|
+
|
|
58
|
+
$ deployctl deploy --json # exit code 2
|
|
59
|
+
{"ok":false,"error":{"code":"MISSING_ARGUMENT","message":"missing required argument 'env'","suggestion":"Run \"deployctl describe deploy\" to see accepted arguments and options."}}
|
|
60
|
+
|
|
61
|
+
$ deployctl describe deploy --json
|
|
62
|
+
{"ok":true,"data":{"name":"deploy","description":"Deploy an environment","usage":"deployctl deploy [options] <env>","arguments":[{"name":"env","description":"environment name","required":true}],"options":[{"flags":"--dry-run","description":"show the plan without deploying"}]}}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
What a human sees (same CLI, no agent detected):
|
|
66
|
+
|
|
67
|
+
```console
|
|
68
|
+
$ deployctl deploy mars
|
|
69
|
+
error: Unknown environment "mars"
|
|
70
|
+
hint: Use one of: staging, prod.
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API
|
|
74
|
+
|
|
75
|
+
### `agentMode(program, options?)`
|
|
76
|
+
|
|
77
|
+
Call right after `new Command()`, **before** defining subcommands. Adds the `--json` flag and the `describe` command, and lets `.action()` handlers return a result.
|
|
78
|
+
|
|
79
|
+
| option | default | |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `autoDetect` | `true` | enable JSON when an AI agent is detected |
|
|
82
|
+
| `describeCommand` | `'describe'` | name of the schema command, `false` to disable |
|
|
83
|
+
| `formatHuman` | strings as-is, other values as pretty JSON | `(data, command) => string \| undefined` |
|
|
84
|
+
|
|
85
|
+
### `run(program, options?)` → `Promise<{ mode, exitCode, envelope }>`
|
|
86
|
+
|
|
87
|
+
Parses `process.argv`, runs the action, prints the result/error, sets `process.exitCode`. For tests pass `args`, `env`, `stdout`, `stderr`, `json`, `setExitCode: false`.
|
|
88
|
+
|
|
89
|
+
### `new AgentError(code, message, { suggestion?, details?, exitCode? })`
|
|
90
|
+
|
|
91
|
+
Throw it from an action for a structured error. Anything else thrown becomes `INTERNAL_ERROR`.
|
|
92
|
+
|
|
93
|
+
### `describeCommand(cmd)`, `detectAgent(env?)`
|
|
94
|
+
|
|
95
|
+
Lower-level helpers: the schema generator and the agent detector.
|
|
96
|
+
|
|
97
|
+
### Error codes from commander
|
|
98
|
+
|
|
99
|
+
`UNKNOWN_OPTION`, `UNKNOWN_COMMAND`, `MISSING_ARGUMENT`, `MISSING_OPTION_VALUE`, `MISSING_REQUIRED_OPTION`, `INVALID_ARGUMENT`, `EXCESS_ARGUMENTS`, `CONFLICTING_OPTIONS`, `MISSING_COMMAND`, `USAGE_ERROR`.
|
|
100
|
+
|
|
101
|
+
## Benchmark: does it actually help an agent?
|
|
102
|
+
|
|
103
|
+
Same tiny notes CLI, written twice: plain commander vs. with `cli-agent-mode`. Claude Code (headless, Claude Haiku) gets the same task: add notes, mark one done, delete a tag (the CLI refuses to delete unfinished notes without `--force`), hit a missing id, report the result. 6 runs per variant:
|
|
104
|
+
|
|
105
|
+
| | plain commander | cli-agent-mode |
|
|
106
|
+
|---|:-:|:-:|
|
|
107
|
+
| task completed | 6/6 | 6/6 |
|
|
108
|
+
| CLI calls (avg, range) | 17.3 (16–19) | **11.3 (9–12)** |
|
|
109
|
+
| `--help` calls per run | 4 | **1** |
|
|
110
|
+
| failed calls (avg) | 3.0 | **1.8** |
|
|
111
|
+
| CLI output (avg bytes) | ~1100 | ~2000 |
|
|
112
|
+
|
|
113
|
+
**−35% CLI calls with the same success rate.** The agent learns the whole CLI from one `--help` (which returns the schema of every command) instead of reading help page by page, and recovers from errors using `code` + `suggestion`. The trade-off is ~1.8× more output (≈250 tokens per task here).
|
|
114
|
+
|
|
115
|
+
Reproduce: [`examples/playground/agent-bench.sh`](examples/playground/agent-bench.sh) `5 haiku` (requires Claude Code).
|
|
116
|
+
|
|
117
|
+
## Why not ...
|
|
118
|
+
|
|
119
|
+
| | `--json` from return values | structured errors + suggestion | schema command | agent auto-detect |
|
|
120
|
+
|---|:-:|:-:|:-:|:-:|
|
|
121
|
+
| plain commander | ✗ | ✗ | ✗ | ✗ |
|
|
122
|
+
| oclif (`enableJsonFlag`) | ✓ | partial | ✗ | ✗ |
|
|
123
|
+
| hand-rolled `if (opts.json)` | per command | usually ✗ | ✗ | ✗ |
|
|
124
|
+
| **cli-agent-mode** | ✓ | ✓ | ✓ | ✓ |
|
|
125
|
+
|
|
126
|
+
If you are already on oclif, you have `--json`. If you are on commander — the most used Node CLI framework — this is the missing piece.
|
|
127
|
+
|
|
128
|
+
## Limitations
|
|
129
|
+
|
|
130
|
+
- Subcommands must be created with `program.command()` (or added via `addCommand()` *before* their `.action()` is set) so return values can be captured.
|
|
131
|
+
- `--json` is reserved: it is removed from argv before parsing (except after `--`).
|
|
132
|
+
- In JSON mode `console.log/info/debug` are redirected to stderr, but direct `process.stdout.write` calls inside actions are not — return data instead.
|
|
133
|
+
- Typed `@commander-js/extra-typings` is not supported yet.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let commander = require("commander");
|
|
3
|
+
let node_util = require("node:util");
|
|
4
|
+
//#region src/detect.ts
|
|
5
|
+
/**
|
|
6
|
+
* Synchronous, dependency-free detection of AI coding agents.
|
|
7
|
+
* Env variables mirror @vercel/detect-agent (Nov 2026) plus the `AI_AGENT` convention.
|
|
8
|
+
* CURSOR_TRACE_ID is deliberately ignored: it is also set in Cursor's terminal for humans.
|
|
9
|
+
* Returns the agent name or `undefined`.
|
|
10
|
+
*/
|
|
11
|
+
function detectAgent(env = process.env) {
|
|
12
|
+
const aiAgent = env.AI_AGENT?.trim();
|
|
13
|
+
if (aiAgent) return aiAgent;
|
|
14
|
+
if (env.CLAUDECODE || env.CLAUDE_CODE) return "claude";
|
|
15
|
+
if (env.CURSOR_AGENT || env.CURSOR_EXTENSION_HOST_ROLE === "agent-exec") return "cursor-cli";
|
|
16
|
+
if (env.GEMINI_CLI) return "gemini";
|
|
17
|
+
if (env.CODEX_SANDBOX || env.CODEX_CI || env.CODEX_THREAD_ID) return "codex";
|
|
18
|
+
if (env.ANTIGRAVITY_AGENT) return "antigravity";
|
|
19
|
+
if (env.AUGMENT_AGENT) return "augment-cli";
|
|
20
|
+
if (env.OPENCODE === "1" || env.OPENCODE_CLIENT) return "opencode";
|
|
21
|
+
if (env.COPILOT_MODEL || env.COPILOT_ALLOW_ALL) return "github-copilot";
|
|
22
|
+
if (env.REPL_ID) return "replit";
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/describe.ts
|
|
26
|
+
const clean = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0 && v !== "" && v !== false && !(Array.isArray(v) && v.length === 0)));
|
|
27
|
+
function describeArgument(a) {
|
|
28
|
+
return clean({
|
|
29
|
+
name: a.name(),
|
|
30
|
+
description: a.description,
|
|
31
|
+
required: a.required || void 0,
|
|
32
|
+
variadic: a.variadic || void 0,
|
|
33
|
+
default: a.defaultValue,
|
|
34
|
+
choices: a.argChoices
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function describeOption(o) {
|
|
38
|
+
return clean({
|
|
39
|
+
flags: o.flags,
|
|
40
|
+
description: o.description,
|
|
41
|
+
required: o.mandatory || void 0,
|
|
42
|
+
default: o.defaultValue,
|
|
43
|
+
choices: o.argChoices,
|
|
44
|
+
env: o.envVar
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function commandPath(cmd) {
|
|
48
|
+
const parts = [];
|
|
49
|
+
for (let c = cmd; c; c = c.parent) parts.unshift(c.name());
|
|
50
|
+
return parts.join(" ");
|
|
51
|
+
}
|
|
52
|
+
const isHidden = (c) => c._hidden === true;
|
|
53
|
+
/** Compact JSON schema of a command and all of its visible subcommands. */
|
|
54
|
+
function describeCommand(cmd) {
|
|
55
|
+
return clean({
|
|
56
|
+
name: cmd.name(),
|
|
57
|
+
description: cmd.description(),
|
|
58
|
+
usage: `${commandPath(cmd)} ${cmd.usage()}`.trim(),
|
|
59
|
+
aliases: cmd.aliases(),
|
|
60
|
+
arguments: cmd.registeredArguments.map(describeArgument),
|
|
61
|
+
options: cmd.options.filter((o) => !o.hidden).map(describeOption),
|
|
62
|
+
commands: cmd.commands.filter((c) => !isHidden(c)).map(describeCommand)
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** Resolve `["deploy", "preview"]` to the nested subcommand (by name or alias). */
|
|
66
|
+
function findCommand(root, path) {
|
|
67
|
+
let cur = root;
|
|
68
|
+
for (const part of path) {
|
|
69
|
+
cur = cur?.commands.find((c) => c.name() === part || c.aliases().includes(part));
|
|
70
|
+
if (!cur) return void 0;
|
|
71
|
+
}
|
|
72
|
+
return cur;
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/errors.ts
|
|
76
|
+
/**
|
|
77
|
+
* An error with a stable machine-readable `code`.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* throw new AgentError('ENV_NOT_FOUND', `Environment "${env}" does not exist`, {
|
|
81
|
+
* suggestion: 'Run "mycli env list" to see available environments.',
|
|
82
|
+
* });
|
|
83
|
+
*/
|
|
84
|
+
var AgentError = class extends Error {
|
|
85
|
+
code;
|
|
86
|
+
suggestion;
|
|
87
|
+
details;
|
|
88
|
+
exitCode;
|
|
89
|
+
constructor(code, message, options = {}) {
|
|
90
|
+
super(message);
|
|
91
|
+
this.name = "AgentError";
|
|
92
|
+
this.code = code;
|
|
93
|
+
this.suggestion = options.suggestion;
|
|
94
|
+
this.details = options.details;
|
|
95
|
+
this.exitCode = options.exitCode ?? 1;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
/** Exit codes used by cli-agent-mode. */
|
|
99
|
+
const EXIT = {
|
|
100
|
+
OK: 0,
|
|
101
|
+
ERROR: 1,
|
|
102
|
+
USAGE: 2
|
|
103
|
+
};
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/agent-mode.ts
|
|
106
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
107
|
+
const rootOf = (cmd) => {
|
|
108
|
+
let c = cmd;
|
|
109
|
+
while (c.parent) c = c.parent;
|
|
110
|
+
return c;
|
|
111
|
+
};
|
|
112
|
+
/** Make every `.action()` handler's return value available as the command result. */
|
|
113
|
+
function patch(cmd) {
|
|
114
|
+
const proto = Object.getPrototypeOf(cmd);
|
|
115
|
+
cmd.action = function(fn) {
|
|
116
|
+
return proto.action.call(this, async function(...args) {
|
|
117
|
+
const value = await fn.apply(this, args);
|
|
118
|
+
const state = states.get(rootOf(this));
|
|
119
|
+
if (state) state.result = {
|
|
120
|
+
value,
|
|
121
|
+
command: this
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
cmd.createCommand = (name) => patch(proto.createCommand.call(cmd, name));
|
|
126
|
+
const addCommand = cmd.addCommand;
|
|
127
|
+
cmd.addCommand = function(sub, opts) {
|
|
128
|
+
walk(sub, patch);
|
|
129
|
+
return addCommand.call(this, sub, opts);
|
|
130
|
+
};
|
|
131
|
+
return cmd;
|
|
132
|
+
}
|
|
133
|
+
function walk(cmd, fn) {
|
|
134
|
+
fn(cmd);
|
|
135
|
+
for (const sub of cmd.commands) walk(sub, fn);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Turn a commander program into an agent-friendly CLI.
|
|
139
|
+
* Call it right after creating the program, before defining subcommands.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* const program = agentMode(new Command('mycli'));
|
|
143
|
+
* program.command('deploy <env>').action(async (env) => ({ url: `https://${env}.example.com` }));
|
|
144
|
+
* await run(program);
|
|
145
|
+
*/
|
|
146
|
+
function agentMode(program, options = {}) {
|
|
147
|
+
const state = { options: {
|
|
148
|
+
autoDetect: options.autoDetect ?? true,
|
|
149
|
+
describeCommand: options.describeCommand ?? "describe",
|
|
150
|
+
formatHuman: options.formatHuman
|
|
151
|
+
} };
|
|
152
|
+
states.set(program, state);
|
|
153
|
+
walk(program, patch);
|
|
154
|
+
program.option("--json", "print one JSON envelope on stdout (auto-on for AI agents)");
|
|
155
|
+
const name = state.options.describeCommand;
|
|
156
|
+
if (name) program.command(`${name} [command...]`).description("print the CLI schema as JSON").action((path = []) => {
|
|
157
|
+
const target = findCommand(program, path);
|
|
158
|
+
if (!target) throw new AgentError("UNKNOWN_COMMAND", `Unknown command: ${path.join(" ")}`, {
|
|
159
|
+
suggestion: `Run "${program.name()} ${name}" to list all commands.`,
|
|
160
|
+
exitCode: EXIT.USAGE
|
|
161
|
+
});
|
|
162
|
+
return describeCommand(target);
|
|
163
|
+
});
|
|
164
|
+
return program;
|
|
165
|
+
}
|
|
166
|
+
const USAGE_CODES = {
|
|
167
|
+
"commander.unknownOption": "UNKNOWN_OPTION",
|
|
168
|
+
"commander.unknownCommand": "UNKNOWN_COMMAND",
|
|
169
|
+
"commander.missingArgument": "MISSING_ARGUMENT",
|
|
170
|
+
"commander.optionMissingArgument": "MISSING_OPTION_VALUE",
|
|
171
|
+
"commander.missingMandatoryOptionValue": "MISSING_REQUIRED_OPTION",
|
|
172
|
+
"commander.invalidArgument": "INVALID_ARGUMENT",
|
|
173
|
+
"commander.excessArguments": "EXCESS_ARGUMENTS",
|
|
174
|
+
"commander.conflictingOption": "CONFLICTING_OPTIONS",
|
|
175
|
+
"commander.help": "MISSING_COMMAND"
|
|
176
|
+
};
|
|
177
|
+
function extractJsonFlag(args) {
|
|
178
|
+
const end = args.indexOf("--");
|
|
179
|
+
const head = end === -1 ? args : args.slice(0, end);
|
|
180
|
+
const tail = end === -1 ? [] : args.slice(end);
|
|
181
|
+
const json = head.includes("--json");
|
|
182
|
+
return {
|
|
183
|
+
args: [...head.filter((a) => a !== "--json"), ...tail],
|
|
184
|
+
json
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const defaultHuman = (data) => data === void 0 || data === null ? void 0 : typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
188
|
+
/** Parse arguments, run the matching action and print the result or error in the right format. */
|
|
189
|
+
async function run(program, runOptions = {}) {
|
|
190
|
+
const state = states.get(program);
|
|
191
|
+
if (!state) throw new Error("cli-agent-mode: call agentMode(program) before run(program)");
|
|
192
|
+
state.result = void 0;
|
|
193
|
+
const env = runOptions.env ?? process.env;
|
|
194
|
+
const stdout = runOptions.stdout ?? process.stdout;
|
|
195
|
+
const stderr = runOptions.stderr ?? process.stderr;
|
|
196
|
+
const extracted = extractJsonFlag(runOptions.args ?? process.argv.slice(2));
|
|
197
|
+
const forced = env.CLI_AGENT_MODE === "json" ? true : env.CLI_AGENT_MODE === "human" ? false : void 0;
|
|
198
|
+
const json = runOptions.json ?? (extracted.json || (forced ?? (state.options.autoDetect && detectAgent(env) !== void 0)));
|
|
199
|
+
const mode = json ? "json" : "human";
|
|
200
|
+
let outBuf = "";
|
|
201
|
+
let errorCommand = program;
|
|
202
|
+
const restore = [];
|
|
203
|
+
walk(program, (cmd) => {
|
|
204
|
+
const prevOutput = cmd.configureOutput();
|
|
205
|
+
const prevHelp = cmd.configureHelp();
|
|
206
|
+
restore.push(() => {
|
|
207
|
+
cmd.configureOutput(prevOutput);
|
|
208
|
+
cmd.configureHelp(prevHelp);
|
|
209
|
+
});
|
|
210
|
+
cmd.exitOverride();
|
|
211
|
+
cmd.configureOutput({
|
|
212
|
+
...prevOutput,
|
|
213
|
+
writeOut: (s) => json ? outBuf += s : stdout.write(s),
|
|
214
|
+
writeErr: (s) => json ? void 0 : stderr.write(s),
|
|
215
|
+
outputError: (s, write) => {
|
|
216
|
+
errorCommand = cmd;
|
|
217
|
+
if (!json) write(s);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
if (json) cmd.configureHelp({
|
|
221
|
+
...prevHelp,
|
|
222
|
+
formatHelp: (c) => JSON.stringify({
|
|
223
|
+
ok: true,
|
|
224
|
+
data: describeCommand(c)
|
|
225
|
+
}) + "\n"
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
const consoleKeys = [
|
|
229
|
+
"log",
|
|
230
|
+
"info",
|
|
231
|
+
"debug"
|
|
232
|
+
];
|
|
233
|
+
const savedConsole = consoleKeys.map((k) => console[k]);
|
|
234
|
+
if (json) for (const k of consoleKeys) console[k] = (...a) => void stderr.write((0, node_util.format)(...a) + "\n");
|
|
235
|
+
let envelope;
|
|
236
|
+
let exitCode = EXIT.OK;
|
|
237
|
+
let printed = false;
|
|
238
|
+
try {
|
|
239
|
+
await program.parseAsync(extracted.args, { from: "user" });
|
|
240
|
+
const result = state.result;
|
|
241
|
+
const value = result?.value;
|
|
242
|
+
envelope = {
|
|
243
|
+
ok: true,
|
|
244
|
+
data: value ?? null
|
|
245
|
+
};
|
|
246
|
+
if (!json) {
|
|
247
|
+
const text = (state.options.formatHuman ?? defaultHuman)(value, result?.command ?? program);
|
|
248
|
+
if (text !== void 0) stdout.write(text + "\n");
|
|
249
|
+
printed = true;
|
|
250
|
+
}
|
|
251
|
+
} catch (err) {
|
|
252
|
+
if (err instanceof commander.CommanderError && (err.code === "commander.helpDisplayed" || err.code === "commander.help" && err.exitCode === 0)) {
|
|
253
|
+
if (json) stdout.write(outBuf);
|
|
254
|
+
return finish({
|
|
255
|
+
ok: true,
|
|
256
|
+
data: null
|
|
257
|
+
}, EXIT.OK, true);
|
|
258
|
+
}
|
|
259
|
+
if (err instanceof commander.CommanderError && err.code === "commander.version") {
|
|
260
|
+
envelope = {
|
|
261
|
+
ok: true,
|
|
262
|
+
data: { version: outBuf.trim() || program.version() }
|
|
263
|
+
};
|
|
264
|
+
exitCode = EXIT.OK;
|
|
265
|
+
if (!json) printed = true;
|
|
266
|
+
} else {
|
|
267
|
+
const { payload, code } = toPayload(err, program, errorCommand, state.options.describeCommand);
|
|
268
|
+
envelope = {
|
|
269
|
+
ok: false,
|
|
270
|
+
error: payload
|
|
271
|
+
};
|
|
272
|
+
exitCode = code;
|
|
273
|
+
if (!json) {
|
|
274
|
+
if (!(err instanceof commander.CommanderError)) stderr.write(`error: ${payload.message}\n`);
|
|
275
|
+
if (payload.suggestion) stderr.write(`hint: ${payload.suggestion}\n`);
|
|
276
|
+
printed = true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} finally {
|
|
280
|
+
consoleKeys.forEach((k, i) => console[k] = savedConsole[i]);
|
|
281
|
+
restore.forEach((r) => r());
|
|
282
|
+
}
|
|
283
|
+
return finish(envelope, exitCode, printed);
|
|
284
|
+
function finish(env, code, done) {
|
|
285
|
+
if (json && !done) stdout.write(JSON.stringify(env) + "\n");
|
|
286
|
+
if (runOptions.setExitCode ?? true) process.exitCode = code;
|
|
287
|
+
return {
|
|
288
|
+
mode,
|
|
289
|
+
exitCode: code,
|
|
290
|
+
envelope: env
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function toPayload(err, root, at, describeName) {
|
|
295
|
+
if (err instanceof AgentError) return {
|
|
296
|
+
payload: strip({
|
|
297
|
+
code: err.code,
|
|
298
|
+
message: err.message,
|
|
299
|
+
suggestion: err.suggestion,
|
|
300
|
+
details: err.details
|
|
301
|
+
}),
|
|
302
|
+
code: err.exitCode
|
|
303
|
+
};
|
|
304
|
+
if (err instanceof commander.CommanderError) {
|
|
305
|
+
const sub = commandPath(at).split(" ").slice(1).join(" ");
|
|
306
|
+
const suggestion = describeName ? `Run "${[
|
|
307
|
+
root.name(),
|
|
308
|
+
describeName,
|
|
309
|
+
sub
|
|
310
|
+
].filter(Boolean).join(" ")}" to see accepted arguments and options.` : `Run "${commandPath(at)} --help" to see usage.`;
|
|
311
|
+
const message = err.code === "commander.help" ? "A subcommand is required." : err.message.replace(/^error:\s*/, "");
|
|
312
|
+
return {
|
|
313
|
+
payload: {
|
|
314
|
+
code: USAGE_CODES[err.code] ?? "USAGE_ERROR",
|
|
315
|
+
message,
|
|
316
|
+
suggestion
|
|
317
|
+
},
|
|
318
|
+
code: EXIT.USAGE
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
payload: {
|
|
323
|
+
code: "INTERNAL_ERROR",
|
|
324
|
+
message: err instanceof Error ? err.message : String(err)
|
|
325
|
+
},
|
|
326
|
+
code: EXIT.ERROR
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const strip = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
|
|
330
|
+
//#endregion
|
|
331
|
+
exports.AgentError = AgentError;
|
|
332
|
+
exports.EXIT = EXIT;
|
|
333
|
+
exports.agentMode = agentMode;
|
|
334
|
+
exports.describeCommand = describeCommand;
|
|
335
|
+
exports.detectAgent = detectAgent;
|
|
336
|
+
exports.findCommand = findCommand;
|
|
337
|
+
exports.run = run;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/// <reference path="./commander-augment.d.ts" />
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
|
|
4
|
+
//#region src/errors.d.ts
|
|
5
|
+
interface AgentErrorOptions {
|
|
6
|
+
/** One concrete next step the agent (or human) can follow literally. */
|
|
7
|
+
suggestion?: string;
|
|
8
|
+
/** Any JSON-serializable context: ids, paths, limits, etc. */
|
|
9
|
+
details?: unknown;
|
|
10
|
+
/** Process exit code. Defaults to 1. */
|
|
11
|
+
exitCode?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* An error with a stable machine-readable `code`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* throw new AgentError('ENV_NOT_FOUND', `Environment "${env}" does not exist`, {
|
|
18
|
+
* suggestion: 'Run "mycli env list" to see available environments.',
|
|
19
|
+
* });
|
|
20
|
+
*/
|
|
21
|
+
declare class AgentError extends Error {
|
|
22
|
+
readonly code: string;
|
|
23
|
+
readonly suggestion?: string;
|
|
24
|
+
readonly details?: unknown;
|
|
25
|
+
readonly exitCode: number;
|
|
26
|
+
constructor(code: string, message: string, options?: AgentErrorOptions);
|
|
27
|
+
}
|
|
28
|
+
interface ErrorPayload {
|
|
29
|
+
code: string;
|
|
30
|
+
message: string;
|
|
31
|
+
suggestion?: string;
|
|
32
|
+
details?: unknown;
|
|
33
|
+
}
|
|
34
|
+
type Envelope<T = unknown> = {
|
|
35
|
+
ok: true;
|
|
36
|
+
data: T;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
error: ErrorPayload;
|
|
40
|
+
};
|
|
41
|
+
/** Exit codes used by cli-agent-mode. */
|
|
42
|
+
declare const EXIT: {
|
|
43
|
+
readonly OK: 0;
|
|
44
|
+
readonly ERROR: 1;
|
|
45
|
+
readonly USAGE: 2;
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/agent-mode.d.ts
|
|
49
|
+
interface AgentModeOptions {
|
|
50
|
+
/** Switch to JSON automatically when an AI agent is detected (AI_AGENT, CLAUDECODE, CURSOR_AGENT, ...). Default: true. */
|
|
51
|
+
autoDetect?: boolean;
|
|
52
|
+
/** Name of the self-description command, or `false` to skip adding it. Default: "describe". */
|
|
53
|
+
describeCommand?: string | false;
|
|
54
|
+
/** Render action results for humans. Default: strings as-is, everything else as pretty JSON. */
|
|
55
|
+
formatHuman?: (data: unknown, command: Command) => string | undefined;
|
|
56
|
+
}
|
|
57
|
+
interface RunOptions {
|
|
58
|
+
/** User arguments (without node and script path). Default: process.argv.slice(2). */
|
|
59
|
+
args?: string[];
|
|
60
|
+
/** Force JSON (`true`) or human (`false`) output, bypassing flag and agent detection. */
|
|
61
|
+
json?: boolean;
|
|
62
|
+
env?: NodeJS.ProcessEnv;
|
|
63
|
+
stdout?: {
|
|
64
|
+
write(chunk: string): unknown;
|
|
65
|
+
};
|
|
66
|
+
stderr?: {
|
|
67
|
+
write(chunk: string): unknown;
|
|
68
|
+
};
|
|
69
|
+
/** Set `process.exitCode`. Default: true. */
|
|
70
|
+
setExitCode?: boolean;
|
|
71
|
+
}
|
|
72
|
+
interface RunResult {
|
|
73
|
+
mode: 'json' | 'human';
|
|
74
|
+
exitCode: number;
|
|
75
|
+
envelope: Envelope;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Turn a commander program into an agent-friendly CLI.
|
|
79
|
+
* Call it right after creating the program, before defining subcommands.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* const program = agentMode(new Command('mycli'));
|
|
83
|
+
* program.command('deploy <env>').action(async (env) => ({ url: `https://${env}.example.com` }));
|
|
84
|
+
* await run(program);
|
|
85
|
+
*/
|
|
86
|
+
declare function agentMode<T extends Command>(program: T, options?: AgentModeOptions): T;
|
|
87
|
+
/** Parse arguments, run the matching action and print the result or error in the right format. */
|
|
88
|
+
declare function run(program: Command, runOptions?: RunOptions): Promise<RunResult>;
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/describe.d.ts
|
|
91
|
+
interface ArgumentSchema {
|
|
92
|
+
name: string;
|
|
93
|
+
description?: string;
|
|
94
|
+
/** present only when true */
|
|
95
|
+
required?: true;
|
|
96
|
+
/** present only when true */
|
|
97
|
+
variadic?: true;
|
|
98
|
+
default?: unknown;
|
|
99
|
+
choices?: readonly string[];
|
|
100
|
+
}
|
|
101
|
+
interface OptionSchema {
|
|
102
|
+
/** commander flags, e.g. "-t, --tag <tag>" */
|
|
103
|
+
flags: string;
|
|
104
|
+
description?: string;
|
|
105
|
+
/** present only when the option itself must be passed */
|
|
106
|
+
required?: true;
|
|
107
|
+
default?: unknown;
|
|
108
|
+
choices?: readonly string[];
|
|
109
|
+
env?: string;
|
|
110
|
+
}
|
|
111
|
+
interface CommandSchema {
|
|
112
|
+
name: string;
|
|
113
|
+
description?: string;
|
|
114
|
+
/** full invocation, e.g. "mycli deploy [options] <env>" */
|
|
115
|
+
usage: string;
|
|
116
|
+
aliases?: string[];
|
|
117
|
+
arguments?: ArgumentSchema[];
|
|
118
|
+
options?: OptionSchema[];
|
|
119
|
+
commands?: CommandSchema[];
|
|
120
|
+
}
|
|
121
|
+
/** Compact JSON schema of a command and all of its visible subcommands. */
|
|
122
|
+
declare function describeCommand(cmd: Command): CommandSchema;
|
|
123
|
+
/** Resolve `["deploy", "preview"]` to the nested subcommand (by name or alias). */
|
|
124
|
+
declare function findCommand(root: Command, path: string[]): Command | undefined;
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/detect.d.ts
|
|
127
|
+
/**
|
|
128
|
+
* Synchronous, dependency-free detection of AI coding agents.
|
|
129
|
+
* Env variables mirror @vercel/detect-agent (Nov 2026) plus the `AI_AGENT` convention.
|
|
130
|
+
* CURSOR_TRACE_ID is deliberately ignored: it is also set in Cursor's terminal for humans.
|
|
131
|
+
* Returns the agent name or `undefined`.
|
|
132
|
+
*/
|
|
133
|
+
declare function detectAgent(env?: NodeJS.ProcessEnv): string | undefined;
|
|
134
|
+
//#endregion
|
|
135
|
+
export { AgentError, type AgentErrorOptions, type AgentModeOptions, type ArgumentSchema, type CommandSchema, EXIT, type Envelope, type ErrorPayload, type OptionSchema, type RunOptions, type RunResult, agentMode, describeCommand, detectAgent, findCommand, run };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/// <reference path="./commander-augment.d.ts" />
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
|
|
4
|
+
//#region src/errors.d.ts
|
|
5
|
+
interface AgentErrorOptions {
|
|
6
|
+
/** One concrete next step the agent (or human) can follow literally. */
|
|
7
|
+
suggestion?: string;
|
|
8
|
+
/** Any JSON-serializable context: ids, paths, limits, etc. */
|
|
9
|
+
details?: unknown;
|
|
10
|
+
/** Process exit code. Defaults to 1. */
|
|
11
|
+
exitCode?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* An error with a stable machine-readable `code`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* throw new AgentError('ENV_NOT_FOUND', `Environment "${env}" does not exist`, {
|
|
18
|
+
* suggestion: 'Run "mycli env list" to see available environments.',
|
|
19
|
+
* });
|
|
20
|
+
*/
|
|
21
|
+
declare class AgentError extends Error {
|
|
22
|
+
readonly code: string;
|
|
23
|
+
readonly suggestion?: string;
|
|
24
|
+
readonly details?: unknown;
|
|
25
|
+
readonly exitCode: number;
|
|
26
|
+
constructor(code: string, message: string, options?: AgentErrorOptions);
|
|
27
|
+
}
|
|
28
|
+
interface ErrorPayload {
|
|
29
|
+
code: string;
|
|
30
|
+
message: string;
|
|
31
|
+
suggestion?: string;
|
|
32
|
+
details?: unknown;
|
|
33
|
+
}
|
|
34
|
+
type Envelope<T = unknown> = {
|
|
35
|
+
ok: true;
|
|
36
|
+
data: T;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
error: ErrorPayload;
|
|
40
|
+
};
|
|
41
|
+
/** Exit codes used by cli-agent-mode. */
|
|
42
|
+
declare const EXIT: {
|
|
43
|
+
readonly OK: 0;
|
|
44
|
+
readonly ERROR: 1;
|
|
45
|
+
readonly USAGE: 2;
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/agent-mode.d.ts
|
|
49
|
+
interface AgentModeOptions {
|
|
50
|
+
/** Switch to JSON automatically when an AI agent is detected (AI_AGENT, CLAUDECODE, CURSOR_AGENT, ...). Default: true. */
|
|
51
|
+
autoDetect?: boolean;
|
|
52
|
+
/** Name of the self-description command, or `false` to skip adding it. Default: "describe". */
|
|
53
|
+
describeCommand?: string | false;
|
|
54
|
+
/** Render action results for humans. Default: strings as-is, everything else as pretty JSON. */
|
|
55
|
+
formatHuman?: (data: unknown, command: Command) => string | undefined;
|
|
56
|
+
}
|
|
57
|
+
interface RunOptions {
|
|
58
|
+
/** User arguments (without node and script path). Default: process.argv.slice(2). */
|
|
59
|
+
args?: string[];
|
|
60
|
+
/** Force JSON (`true`) or human (`false`) output, bypassing flag and agent detection. */
|
|
61
|
+
json?: boolean;
|
|
62
|
+
env?: NodeJS.ProcessEnv;
|
|
63
|
+
stdout?: {
|
|
64
|
+
write(chunk: string): unknown;
|
|
65
|
+
};
|
|
66
|
+
stderr?: {
|
|
67
|
+
write(chunk: string): unknown;
|
|
68
|
+
};
|
|
69
|
+
/** Set `process.exitCode`. Default: true. */
|
|
70
|
+
setExitCode?: boolean;
|
|
71
|
+
}
|
|
72
|
+
interface RunResult {
|
|
73
|
+
mode: 'json' | 'human';
|
|
74
|
+
exitCode: number;
|
|
75
|
+
envelope: Envelope;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Turn a commander program into an agent-friendly CLI.
|
|
79
|
+
* Call it right after creating the program, before defining subcommands.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* const program = agentMode(new Command('mycli'));
|
|
83
|
+
* program.command('deploy <env>').action(async (env) => ({ url: `https://${env}.example.com` }));
|
|
84
|
+
* await run(program);
|
|
85
|
+
*/
|
|
86
|
+
declare function agentMode<T extends Command>(program: T, options?: AgentModeOptions): T;
|
|
87
|
+
/** Parse arguments, run the matching action and print the result or error in the right format. */
|
|
88
|
+
declare function run(program: Command, runOptions?: RunOptions): Promise<RunResult>;
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/describe.d.ts
|
|
91
|
+
interface ArgumentSchema {
|
|
92
|
+
name: string;
|
|
93
|
+
description?: string;
|
|
94
|
+
/** present only when true */
|
|
95
|
+
required?: true;
|
|
96
|
+
/** present only when true */
|
|
97
|
+
variadic?: true;
|
|
98
|
+
default?: unknown;
|
|
99
|
+
choices?: readonly string[];
|
|
100
|
+
}
|
|
101
|
+
interface OptionSchema {
|
|
102
|
+
/** commander flags, e.g. "-t, --tag <tag>" */
|
|
103
|
+
flags: string;
|
|
104
|
+
description?: string;
|
|
105
|
+
/** present only when the option itself must be passed */
|
|
106
|
+
required?: true;
|
|
107
|
+
default?: unknown;
|
|
108
|
+
choices?: readonly string[];
|
|
109
|
+
env?: string;
|
|
110
|
+
}
|
|
111
|
+
interface CommandSchema {
|
|
112
|
+
name: string;
|
|
113
|
+
description?: string;
|
|
114
|
+
/** full invocation, e.g. "mycli deploy [options] <env>" */
|
|
115
|
+
usage: string;
|
|
116
|
+
aliases?: string[];
|
|
117
|
+
arguments?: ArgumentSchema[];
|
|
118
|
+
options?: OptionSchema[];
|
|
119
|
+
commands?: CommandSchema[];
|
|
120
|
+
}
|
|
121
|
+
/** Compact JSON schema of a command and all of its visible subcommands. */
|
|
122
|
+
declare function describeCommand(cmd: Command): CommandSchema;
|
|
123
|
+
/** Resolve `["deploy", "preview"]` to the nested subcommand (by name or alias). */
|
|
124
|
+
declare function findCommand(root: Command, path: string[]): Command | undefined;
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/detect.d.ts
|
|
127
|
+
/**
|
|
128
|
+
* Synchronous, dependency-free detection of AI coding agents.
|
|
129
|
+
* Env variables mirror @vercel/detect-agent (Nov 2026) plus the `AI_AGENT` convention.
|
|
130
|
+
* CURSOR_TRACE_ID is deliberately ignored: it is also set in Cursor's terminal for humans.
|
|
131
|
+
* Returns the agent name or `undefined`.
|
|
132
|
+
*/
|
|
133
|
+
declare function detectAgent(env?: NodeJS.ProcessEnv): string | undefined;
|
|
134
|
+
//#endregion
|
|
135
|
+
export { AgentError, type AgentErrorOptions, type AgentModeOptions, type ArgumentSchema, type CommandSchema, EXIT, type Envelope, type ErrorPayload, type OptionSchema, type RunOptions, type RunResult, agentMode, describeCommand, detectAgent, findCommand, run };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { CommanderError } from "commander";
|
|
2
|
+
import { format } from "node:util";
|
|
3
|
+
//#region src/detect.ts
|
|
4
|
+
/**
|
|
5
|
+
* Synchronous, dependency-free detection of AI coding agents.
|
|
6
|
+
* Env variables mirror @vercel/detect-agent (Nov 2026) plus the `AI_AGENT` convention.
|
|
7
|
+
* CURSOR_TRACE_ID is deliberately ignored: it is also set in Cursor's terminal for humans.
|
|
8
|
+
* Returns the agent name or `undefined`.
|
|
9
|
+
*/
|
|
10
|
+
function detectAgent(env = process.env) {
|
|
11
|
+
const aiAgent = env.AI_AGENT?.trim();
|
|
12
|
+
if (aiAgent) return aiAgent;
|
|
13
|
+
if (env.CLAUDECODE || env.CLAUDE_CODE) return "claude";
|
|
14
|
+
if (env.CURSOR_AGENT || env.CURSOR_EXTENSION_HOST_ROLE === "agent-exec") return "cursor-cli";
|
|
15
|
+
if (env.GEMINI_CLI) return "gemini";
|
|
16
|
+
if (env.CODEX_SANDBOX || env.CODEX_CI || env.CODEX_THREAD_ID) return "codex";
|
|
17
|
+
if (env.ANTIGRAVITY_AGENT) return "antigravity";
|
|
18
|
+
if (env.AUGMENT_AGENT) return "augment-cli";
|
|
19
|
+
if (env.OPENCODE === "1" || env.OPENCODE_CLIENT) return "opencode";
|
|
20
|
+
if (env.COPILOT_MODEL || env.COPILOT_ALLOW_ALL) return "github-copilot";
|
|
21
|
+
if (env.REPL_ID) return "replit";
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/describe.ts
|
|
25
|
+
const clean = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0 && v !== "" && v !== false && !(Array.isArray(v) && v.length === 0)));
|
|
26
|
+
function describeArgument(a) {
|
|
27
|
+
return clean({
|
|
28
|
+
name: a.name(),
|
|
29
|
+
description: a.description,
|
|
30
|
+
required: a.required || void 0,
|
|
31
|
+
variadic: a.variadic || void 0,
|
|
32
|
+
default: a.defaultValue,
|
|
33
|
+
choices: a.argChoices
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function describeOption(o) {
|
|
37
|
+
return clean({
|
|
38
|
+
flags: o.flags,
|
|
39
|
+
description: o.description,
|
|
40
|
+
required: o.mandatory || void 0,
|
|
41
|
+
default: o.defaultValue,
|
|
42
|
+
choices: o.argChoices,
|
|
43
|
+
env: o.envVar
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function commandPath(cmd) {
|
|
47
|
+
const parts = [];
|
|
48
|
+
for (let c = cmd; c; c = c.parent) parts.unshift(c.name());
|
|
49
|
+
return parts.join(" ");
|
|
50
|
+
}
|
|
51
|
+
const isHidden = (c) => c._hidden === true;
|
|
52
|
+
/** Compact JSON schema of a command and all of its visible subcommands. */
|
|
53
|
+
function describeCommand(cmd) {
|
|
54
|
+
return clean({
|
|
55
|
+
name: cmd.name(),
|
|
56
|
+
description: cmd.description(),
|
|
57
|
+
usage: `${commandPath(cmd)} ${cmd.usage()}`.trim(),
|
|
58
|
+
aliases: cmd.aliases(),
|
|
59
|
+
arguments: cmd.registeredArguments.map(describeArgument),
|
|
60
|
+
options: cmd.options.filter((o) => !o.hidden).map(describeOption),
|
|
61
|
+
commands: cmd.commands.filter((c) => !isHidden(c)).map(describeCommand)
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** Resolve `["deploy", "preview"]` to the nested subcommand (by name or alias). */
|
|
65
|
+
function findCommand(root, path) {
|
|
66
|
+
let cur = root;
|
|
67
|
+
for (const part of path) {
|
|
68
|
+
cur = cur?.commands.find((c) => c.name() === part || c.aliases().includes(part));
|
|
69
|
+
if (!cur) return void 0;
|
|
70
|
+
}
|
|
71
|
+
return cur;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/errors.ts
|
|
75
|
+
/**
|
|
76
|
+
* An error with a stable machine-readable `code`.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* throw new AgentError('ENV_NOT_FOUND', `Environment "${env}" does not exist`, {
|
|
80
|
+
* suggestion: 'Run "mycli env list" to see available environments.',
|
|
81
|
+
* });
|
|
82
|
+
*/
|
|
83
|
+
var AgentError = class extends Error {
|
|
84
|
+
code;
|
|
85
|
+
suggestion;
|
|
86
|
+
details;
|
|
87
|
+
exitCode;
|
|
88
|
+
constructor(code, message, options = {}) {
|
|
89
|
+
super(message);
|
|
90
|
+
this.name = "AgentError";
|
|
91
|
+
this.code = code;
|
|
92
|
+
this.suggestion = options.suggestion;
|
|
93
|
+
this.details = options.details;
|
|
94
|
+
this.exitCode = options.exitCode ?? 1;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/** Exit codes used by cli-agent-mode. */
|
|
98
|
+
const EXIT = {
|
|
99
|
+
OK: 0,
|
|
100
|
+
ERROR: 1,
|
|
101
|
+
USAGE: 2
|
|
102
|
+
};
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/agent-mode.ts
|
|
105
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
106
|
+
const rootOf = (cmd) => {
|
|
107
|
+
let c = cmd;
|
|
108
|
+
while (c.parent) c = c.parent;
|
|
109
|
+
return c;
|
|
110
|
+
};
|
|
111
|
+
/** Make every `.action()` handler's return value available as the command result. */
|
|
112
|
+
function patch(cmd) {
|
|
113
|
+
const proto = Object.getPrototypeOf(cmd);
|
|
114
|
+
cmd.action = function(fn) {
|
|
115
|
+
return proto.action.call(this, async function(...args) {
|
|
116
|
+
const value = await fn.apply(this, args);
|
|
117
|
+
const state = states.get(rootOf(this));
|
|
118
|
+
if (state) state.result = {
|
|
119
|
+
value,
|
|
120
|
+
command: this
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
cmd.createCommand = (name) => patch(proto.createCommand.call(cmd, name));
|
|
125
|
+
const addCommand = cmd.addCommand;
|
|
126
|
+
cmd.addCommand = function(sub, opts) {
|
|
127
|
+
walk(sub, patch);
|
|
128
|
+
return addCommand.call(this, sub, opts);
|
|
129
|
+
};
|
|
130
|
+
return cmd;
|
|
131
|
+
}
|
|
132
|
+
function walk(cmd, fn) {
|
|
133
|
+
fn(cmd);
|
|
134
|
+
for (const sub of cmd.commands) walk(sub, fn);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Turn a commander program into an agent-friendly CLI.
|
|
138
|
+
* Call it right after creating the program, before defining subcommands.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* const program = agentMode(new Command('mycli'));
|
|
142
|
+
* program.command('deploy <env>').action(async (env) => ({ url: `https://${env}.example.com` }));
|
|
143
|
+
* await run(program);
|
|
144
|
+
*/
|
|
145
|
+
function agentMode(program, options = {}) {
|
|
146
|
+
const state = { options: {
|
|
147
|
+
autoDetect: options.autoDetect ?? true,
|
|
148
|
+
describeCommand: options.describeCommand ?? "describe",
|
|
149
|
+
formatHuman: options.formatHuman
|
|
150
|
+
} };
|
|
151
|
+
states.set(program, state);
|
|
152
|
+
walk(program, patch);
|
|
153
|
+
program.option("--json", "print one JSON envelope on stdout (auto-on for AI agents)");
|
|
154
|
+
const name = state.options.describeCommand;
|
|
155
|
+
if (name) program.command(`${name} [command...]`).description("print the CLI schema as JSON").action((path = []) => {
|
|
156
|
+
const target = findCommand(program, path);
|
|
157
|
+
if (!target) throw new AgentError("UNKNOWN_COMMAND", `Unknown command: ${path.join(" ")}`, {
|
|
158
|
+
suggestion: `Run "${program.name()} ${name}" to list all commands.`,
|
|
159
|
+
exitCode: EXIT.USAGE
|
|
160
|
+
});
|
|
161
|
+
return describeCommand(target);
|
|
162
|
+
});
|
|
163
|
+
return program;
|
|
164
|
+
}
|
|
165
|
+
const USAGE_CODES = {
|
|
166
|
+
"commander.unknownOption": "UNKNOWN_OPTION",
|
|
167
|
+
"commander.unknownCommand": "UNKNOWN_COMMAND",
|
|
168
|
+
"commander.missingArgument": "MISSING_ARGUMENT",
|
|
169
|
+
"commander.optionMissingArgument": "MISSING_OPTION_VALUE",
|
|
170
|
+
"commander.missingMandatoryOptionValue": "MISSING_REQUIRED_OPTION",
|
|
171
|
+
"commander.invalidArgument": "INVALID_ARGUMENT",
|
|
172
|
+
"commander.excessArguments": "EXCESS_ARGUMENTS",
|
|
173
|
+
"commander.conflictingOption": "CONFLICTING_OPTIONS",
|
|
174
|
+
"commander.help": "MISSING_COMMAND"
|
|
175
|
+
};
|
|
176
|
+
function extractJsonFlag(args) {
|
|
177
|
+
const end = args.indexOf("--");
|
|
178
|
+
const head = end === -1 ? args : args.slice(0, end);
|
|
179
|
+
const tail = end === -1 ? [] : args.slice(end);
|
|
180
|
+
const json = head.includes("--json");
|
|
181
|
+
return {
|
|
182
|
+
args: [...head.filter((a) => a !== "--json"), ...tail],
|
|
183
|
+
json
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const defaultHuman = (data) => data === void 0 || data === null ? void 0 : typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
187
|
+
/** Parse arguments, run the matching action and print the result or error in the right format. */
|
|
188
|
+
async function run(program, runOptions = {}) {
|
|
189
|
+
const state = states.get(program);
|
|
190
|
+
if (!state) throw new Error("cli-agent-mode: call agentMode(program) before run(program)");
|
|
191
|
+
state.result = void 0;
|
|
192
|
+
const env = runOptions.env ?? process.env;
|
|
193
|
+
const stdout = runOptions.stdout ?? process.stdout;
|
|
194
|
+
const stderr = runOptions.stderr ?? process.stderr;
|
|
195
|
+
const extracted = extractJsonFlag(runOptions.args ?? process.argv.slice(2));
|
|
196
|
+
const forced = env.CLI_AGENT_MODE === "json" ? true : env.CLI_AGENT_MODE === "human" ? false : void 0;
|
|
197
|
+
const json = runOptions.json ?? (extracted.json || (forced ?? (state.options.autoDetect && detectAgent(env) !== void 0)));
|
|
198
|
+
const mode = json ? "json" : "human";
|
|
199
|
+
let outBuf = "";
|
|
200
|
+
let errorCommand = program;
|
|
201
|
+
const restore = [];
|
|
202
|
+
walk(program, (cmd) => {
|
|
203
|
+
const prevOutput = cmd.configureOutput();
|
|
204
|
+
const prevHelp = cmd.configureHelp();
|
|
205
|
+
restore.push(() => {
|
|
206
|
+
cmd.configureOutput(prevOutput);
|
|
207
|
+
cmd.configureHelp(prevHelp);
|
|
208
|
+
});
|
|
209
|
+
cmd.exitOverride();
|
|
210
|
+
cmd.configureOutput({
|
|
211
|
+
...prevOutput,
|
|
212
|
+
writeOut: (s) => json ? outBuf += s : stdout.write(s),
|
|
213
|
+
writeErr: (s) => json ? void 0 : stderr.write(s),
|
|
214
|
+
outputError: (s, write) => {
|
|
215
|
+
errorCommand = cmd;
|
|
216
|
+
if (!json) write(s);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
if (json) cmd.configureHelp({
|
|
220
|
+
...prevHelp,
|
|
221
|
+
formatHelp: (c) => JSON.stringify({
|
|
222
|
+
ok: true,
|
|
223
|
+
data: describeCommand(c)
|
|
224
|
+
}) + "\n"
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
const consoleKeys = [
|
|
228
|
+
"log",
|
|
229
|
+
"info",
|
|
230
|
+
"debug"
|
|
231
|
+
];
|
|
232
|
+
const savedConsole = consoleKeys.map((k) => console[k]);
|
|
233
|
+
if (json) for (const k of consoleKeys) console[k] = (...a) => void stderr.write(format(...a) + "\n");
|
|
234
|
+
let envelope;
|
|
235
|
+
let exitCode = EXIT.OK;
|
|
236
|
+
let printed = false;
|
|
237
|
+
try {
|
|
238
|
+
await program.parseAsync(extracted.args, { from: "user" });
|
|
239
|
+
const result = state.result;
|
|
240
|
+
const value = result?.value;
|
|
241
|
+
envelope = {
|
|
242
|
+
ok: true,
|
|
243
|
+
data: value ?? null
|
|
244
|
+
};
|
|
245
|
+
if (!json) {
|
|
246
|
+
const text = (state.options.formatHuman ?? defaultHuman)(value, result?.command ?? program);
|
|
247
|
+
if (text !== void 0) stdout.write(text + "\n");
|
|
248
|
+
printed = true;
|
|
249
|
+
}
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (err instanceof CommanderError && (err.code === "commander.helpDisplayed" || err.code === "commander.help" && err.exitCode === 0)) {
|
|
252
|
+
if (json) stdout.write(outBuf);
|
|
253
|
+
return finish({
|
|
254
|
+
ok: true,
|
|
255
|
+
data: null
|
|
256
|
+
}, EXIT.OK, true);
|
|
257
|
+
}
|
|
258
|
+
if (err instanceof CommanderError && err.code === "commander.version") {
|
|
259
|
+
envelope = {
|
|
260
|
+
ok: true,
|
|
261
|
+
data: { version: outBuf.trim() || program.version() }
|
|
262
|
+
};
|
|
263
|
+
exitCode = EXIT.OK;
|
|
264
|
+
if (!json) printed = true;
|
|
265
|
+
} else {
|
|
266
|
+
const { payload, code } = toPayload(err, program, errorCommand, state.options.describeCommand);
|
|
267
|
+
envelope = {
|
|
268
|
+
ok: false,
|
|
269
|
+
error: payload
|
|
270
|
+
};
|
|
271
|
+
exitCode = code;
|
|
272
|
+
if (!json) {
|
|
273
|
+
if (!(err instanceof CommanderError)) stderr.write(`error: ${payload.message}\n`);
|
|
274
|
+
if (payload.suggestion) stderr.write(`hint: ${payload.suggestion}\n`);
|
|
275
|
+
printed = true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} finally {
|
|
279
|
+
consoleKeys.forEach((k, i) => console[k] = savedConsole[i]);
|
|
280
|
+
restore.forEach((r) => r());
|
|
281
|
+
}
|
|
282
|
+
return finish(envelope, exitCode, printed);
|
|
283
|
+
function finish(env, code, done) {
|
|
284
|
+
if (json && !done) stdout.write(JSON.stringify(env) + "\n");
|
|
285
|
+
if (runOptions.setExitCode ?? true) process.exitCode = code;
|
|
286
|
+
return {
|
|
287
|
+
mode,
|
|
288
|
+
exitCode: code,
|
|
289
|
+
envelope: env
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function toPayload(err, root, at, describeName) {
|
|
294
|
+
if (err instanceof AgentError) return {
|
|
295
|
+
payload: strip({
|
|
296
|
+
code: err.code,
|
|
297
|
+
message: err.message,
|
|
298
|
+
suggestion: err.suggestion,
|
|
299
|
+
details: err.details
|
|
300
|
+
}),
|
|
301
|
+
code: err.exitCode
|
|
302
|
+
};
|
|
303
|
+
if (err instanceof CommanderError) {
|
|
304
|
+
const sub = commandPath(at).split(" ").slice(1).join(" ");
|
|
305
|
+
const suggestion = describeName ? `Run "${[
|
|
306
|
+
root.name(),
|
|
307
|
+
describeName,
|
|
308
|
+
sub
|
|
309
|
+
].filter(Boolean).join(" ")}" to see accepted arguments and options.` : `Run "${commandPath(at)} --help" to see usage.`;
|
|
310
|
+
const message = err.code === "commander.help" ? "A subcommand is required." : err.message.replace(/^error:\s*/, "");
|
|
311
|
+
return {
|
|
312
|
+
payload: {
|
|
313
|
+
code: USAGE_CODES[err.code] ?? "USAGE_ERROR",
|
|
314
|
+
message,
|
|
315
|
+
suggestion
|
|
316
|
+
},
|
|
317
|
+
code: EXIT.USAGE
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
payload: {
|
|
322
|
+
code: "INTERNAL_ERROR",
|
|
323
|
+
message: err instanceof Error ? err.message : String(err)
|
|
324
|
+
},
|
|
325
|
+
code: EXIT.ERROR
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const strip = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
|
|
329
|
+
//#endregion
|
|
330
|
+
export { AgentError, EXIT, agentMode, describeCommand, detectAgent, findCommand, run };
|
package/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cli-agent-mode",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Make any commander.js CLI agent-friendly: --json output envelopes, machine-readable errors with suggestions, a `describe` command that emits the CLI schema, and stable exit codes.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"commander",
|
|
8
|
+
"ai-agent",
|
|
9
|
+
"llm",
|
|
10
|
+
"agent",
|
|
11
|
+
"json-output",
|
|
12
|
+
"machine-readable",
|
|
13
|
+
"claude-code",
|
|
14
|
+
"cursor",
|
|
15
|
+
"codex",
|
|
16
|
+
"mcp",
|
|
17
|
+
"exit-codes",
|
|
18
|
+
"structured-errors"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsdown src/index.ts --dts --format esm,cjs && node scripts/postbuild.mjs",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"commander": ">=12"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^20.19.43",
|
|
40
|
+
"commander": "^14.0.3",
|
|
41
|
+
"tsdown": "^0.21.10",
|
|
42
|
+
"tsx": "^4.23.15",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"vitest": "^4.1.12"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/missingbits/cli-agent-mode.git"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/missingbits/cli-agent-mode#readme",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/missingbits/cli-agent-mode/issues"
|
|
53
|
+
},
|
|
54
|
+
"author": "Gordey Levchenko (https://github.com/lgordey)",
|
|
55
|
+
"exports": {
|
|
56
|
+
".": {
|
|
57
|
+
"import": {
|
|
58
|
+
"types": "./dist/index.d.mts",
|
|
59
|
+
"default": "./dist/index.mjs"
|
|
60
|
+
},
|
|
61
|
+
"require": {
|
|
62
|
+
"types": "./dist/index.d.cts",
|
|
63
|
+
"default": "./dist/index.cjs"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"main": "./dist/index.cjs",
|
|
68
|
+
"types": "./dist/index.d.cts"
|
|
69
|
+
}
|