@bunizao/cli-kit 0.6.0 → 0.7.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 +130 -11
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/intents.d.ts +31 -0
- package/dist/intents.js +70 -0
- package/dist/nouns.d.ts +26 -0
- package/dist/nouns.js +19 -0
- package/dist/program.d.ts +2 -0
- package/dist/program.js +17 -0
- package/dist/skills.d.ts +92 -0
- package/dist/skills.js +225 -0
- package/dist/theme.js +2 -2
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
createProgram,
|
|
27
27
|
insertDefaultVerb,
|
|
28
28
|
mutating,
|
|
29
|
+
SHARED_NOUNS,
|
|
29
30
|
type NounSpec,
|
|
30
31
|
} from "@bunizao/cli-kit";
|
|
31
32
|
|
|
@@ -35,17 +36,19 @@ const program = createProgram({
|
|
|
35
36
|
description: "Example CLI",
|
|
36
37
|
});
|
|
37
38
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const courses = program.command("courses").aliases(["units", "projects"]);
|
|
40
|
+
courses.command("list").action(listCourses);
|
|
41
|
+
courses.command("show <course>").action(showCourse);
|
|
42
|
+
const tasks = program.command("tasks");
|
|
43
|
+
tasks.command("list <course>").action(listTasks);
|
|
44
|
+
mutating(tasks.command("set <course> <task> <state>")).action(setTaskState);
|
|
42
45
|
|
|
43
46
|
const nouns: readonly NounSpec[] = [
|
|
47
|
+
SHARED_NOUNS.courses,
|
|
44
48
|
{
|
|
45
|
-
name: "
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
defaultByArity: { 0: "list", 1: "show" },
|
|
49
|
+
name: "tasks",
|
|
50
|
+
verbs: ["list", "set"],
|
|
51
|
+
defaultByArity: { 1: "list" },
|
|
49
52
|
valueFlags: [],
|
|
50
53
|
},
|
|
51
54
|
];
|
|
@@ -142,7 +145,8 @@ preserve that status without reporting it. Usage failures throw for the boundary
|
|
|
142
145
|
## Mutations
|
|
143
146
|
|
|
144
147
|
Call `mutating(command)` for `send`, `submit`, `set`, and `mark-read` commands. The marker is
|
|
145
|
-
included by `commandsJson
|
|
148
|
+
included by `commandsJson`; a CLI that describes its own command tree reads it with
|
|
149
|
+
`isMutating(command)` instead of guessing from command names. Command actions call `confirm` before making an upstream request:
|
|
146
150
|
|
|
147
151
|
```ts
|
|
148
152
|
const shouldApply = await confirm(
|
|
@@ -163,13 +167,128 @@ aliases, positional arguments, options, enum values, nested commands, and `mutat
|
|
|
163
167
|
commands at noun depth must use a verb exported in `VERBS`; unsupported verbs throw. `auth` and
|
|
164
168
|
`skills` are action groups rather than domain nouns and are not assigned a `verb` field.
|
|
165
169
|
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
A top-level command named after a shared noun's alias throws, and so does a verb that shared noun
|
|
171
|
+
does not offer.
|
|
172
|
+
|
|
173
|
+
The exit-code table, error vocabulary, verb set, and shared nouns are public versioned API. Changing
|
|
174
|
+
one requires a major package release.
|
|
168
175
|
|
|
169
176
|
The conformance suite is isolated under `conformance/`. Until all three CLIs have published their
|
|
170
177
|
normalized releases, missing binaries are reported as skipped tests. Its CI workflow installs the
|
|
171
178
|
published packages before running the suite.
|
|
172
179
|
|
|
180
|
+
## Shared nouns
|
|
181
|
+
|
|
182
|
+
`SHARED_NOUNS` holds the nouns every CLI names the same way. A CLI that offers one uses its spec
|
|
183
|
+
as is, or with fewer verbs and its own `valueFlags`; domain nouns such as `tasks` or `lessons`
|
|
184
|
+
stay with each CLI.
|
|
185
|
+
|
|
186
|
+
| Noun | Aliases | Verbs | Default by arity |
|
|
187
|
+
|---|---|---|---|
|
|
188
|
+
| `courses` | `units`, `projects` | list, show, get | 0 list, 1 show |
|
|
189
|
+
| `grades` | | list | 0 list, 1 list |
|
|
190
|
+
|
|
191
|
+
`courses` is canonical because Ed and Moodle both call them courses; `units` is an Australian usage.
|
|
192
|
+
|
|
193
|
+
Each `grades` row carries `grade` and `max` as numbers or `null`, and a `status` from
|
|
194
|
+
`GRADE_STATUSES`: `graded`, `ungraded`, `unreleased` (marked but not released to the student),
|
|
195
|
+
`not_submitted`, or `unavailable` (the platform refused or failed for that row). Display strings
|
|
196
|
+
such as a letter grade or a range are optional extra fields.
|
|
197
|
+
|
|
198
|
+
## Intents, skills and plugins
|
|
199
|
+
|
|
200
|
+
An `Intent` maps what a person says to the command and the MCP tool that answer it. One list of
|
|
201
|
+
intents is the single source for the skill, the MCP tool descriptions and the agent packaging:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
const INTENTS: readonly Intent[] = [
|
|
205
|
+
{
|
|
206
|
+
when: "my grades / what did I get",
|
|
207
|
+
what: "Graded items per course with grade, max and status.",
|
|
208
|
+
command: "example grades [COURSE]",
|
|
209
|
+
tool: "list_grades",
|
|
210
|
+
refs: "course code, name or id",
|
|
211
|
+
then: "show the item for feedback",
|
|
212
|
+
cost: "small",
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
server.registerTool("list_grades", { description: toolDescription(INTENTS[0]) /* , schema */ }, handler);
|
|
217
|
+
|
|
218
|
+
const files = agentFiles({
|
|
219
|
+
skill: {
|
|
220
|
+
name: "example-cli",
|
|
221
|
+
description: "Reads Example courses, grades and deadlines",
|
|
222
|
+
useWhen: 'the user names Example or asks "what\'s due", "my grades"',
|
|
223
|
+
title: "Example CLI",
|
|
224
|
+
intents: INTENTS,
|
|
225
|
+
mcpServer: "example",
|
|
226
|
+
workflows: [{ title: "Submit an assignment", steps: ["...", "Check the submission status"] }],
|
|
227
|
+
program: commandsJson(program),
|
|
228
|
+
},
|
|
229
|
+
version,
|
|
230
|
+
repository: "https://github.com/example/example-cli",
|
|
231
|
+
author: { name: "example" },
|
|
232
|
+
mcpServers: { example: { command: "npx", args: ["-y", "example-cli", "mcp"] } },
|
|
233
|
+
});
|
|
234
|
+
writeAgentFiles(process.cwd(), files); // { check: true } returns stale paths for CI
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`agentFiles` returns, by path from the repository root:
|
|
238
|
+
|
|
239
|
+
- `skills/<name>/SKILL.md`, which `npx skills add owner/repo` and Claude Code both find. An agent
|
|
240
|
+
loads it whole, so it stays short: one table of what the user says against the CLI command and
|
|
241
|
+
the `server:tool` MCP tool, the workflows as copyable checklists, the rules and the setup. A CLI
|
|
242
|
+
with a mutating command gets the shared dry-run, confirm, apply, read-back workflow first. Each
|
|
243
|
+
tool's full description stays with the MCP server.
|
|
244
|
+
- The frontmatter description is `description` then "Use when `useWhen`". `renderSkill` throws
|
|
245
|
+
when the name or description breaks the Agent Skills limits (lowercase hyphenated name, 1024
|
|
246
|
+
characters, no `<` or `>`) or uses "I" or "you" outside quotes.
|
|
247
|
+
- `skills/<name>/references/commands.md`: every command with its arguments, options and whether
|
|
248
|
+
it mutates, plus the exit codes. Pages in `references` are written next to it.
|
|
249
|
+
- `skills/<name>/agents/openai.yaml`, when `openai` metadata is given.
|
|
250
|
+
- `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json`: the repository is a Claude
|
|
251
|
+
Code plugin, which loads `skills/` by default, and its own marketplace. Install with
|
|
252
|
+
`claude plugin marketplace add owner/repo`, then `claude plugin install example-cli@example-cli`.
|
|
253
|
+
A repository with a root `CLAUDE.md` gets one validator warning, since that file is not plugin
|
|
254
|
+
context; `claude plugin validate .` passes, `--strict` does not.
|
|
255
|
+
|
|
256
|
+
`writeAgentFiles` never deletes, so a CLI moving to this layout removes its old root `SKILL.md`,
|
|
257
|
+
`references/` and `agents/` itself.
|
|
258
|
+
|
|
259
|
+
An MCP server that runs on Cloudflare Workers imports `toolDescription` from
|
|
260
|
+
`@bunizao/cli-kit/intents`, which pulls in neither commander nor Node builtins.
|
|
261
|
+
|
|
262
|
+
`checkIntentCommands(program, nouns, intents)` throws when an intent's command no longer parses:
|
|
263
|
+
a renamed noun or verb, a removed option, or the wrong binary name. Call it from the CLI's tests
|
|
264
|
+
so the skill and the MCP descriptions cannot drift from the command tree.
|
|
265
|
+
|
|
266
|
+
### Trigger evals
|
|
267
|
+
|
|
268
|
+
`evals/trigger.mjs` checks whether the descriptions route requests to the right skill. It puts
|
|
269
|
+
every sibling skill in one Codex workspace, so they compete as they do for a student who has all
|
|
270
|
+
three installed. A skill that steals its sibling's requests looks like the sibling undertriggering
|
|
271
|
+
when either is tested alone. For each query it records which `skills/<name>/` Codex opens first.
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
node evals/trigger.mjs \
|
|
275
|
+
--skill ../moodle-cli/skills/moodle-cli \
|
|
276
|
+
--skill ../ontrack-cli/skills/ontrack-cli \
|
|
277
|
+
--skill ../edstem-cli/skills/edstem-cli \
|
|
278
|
+
--runs 3
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
- The folder name is the skill name. `--pool` defaults to `evals/trigger-pool.json`: 29
|
|
282
|
+
requests, each with the skill it should reach or `none`. `--model` defaults to Codex's configured
|
|
283
|
+
model. `--out` keeps every run's pick and first command.
|
|
284
|
+
- Each Codex run is killed at its first command, and each skill's binary (`moodle`, `ontrack`,
|
|
285
|
+
`edstem`) is a failing stub first on `PATH`, so nothing reaches a real account. `HOME` is a
|
|
286
|
+
temporary folder that hides the user's own skills; `CODEX_HOME` keeps the sign-in.
|
|
287
|
+
- A run costs about 16k input tokens, so the default 29 × 3 is about 1.4M. Results move by about
|
|
288
|
+
one run per query between passes, so compare changes across three runs, not one.
|
|
289
|
+
- This measures Codex, not Claude. Claude Code reads the same folder layout and frontmatter, but
|
|
290
|
+
its routing can differ.
|
|
291
|
+
|
|
173
292
|
## Prompts and progress
|
|
174
293
|
|
|
175
294
|
`createUi` gives every CLI the same interactive surface, drawn with `@clack/prompts` on
|
package/dist/index.d.ts
CHANGED
|
@@ -4,9 +4,11 @@ export * from "./color.js";
|
|
|
4
4
|
export * from "./confirm.js";
|
|
5
5
|
export * from "./errors.js";
|
|
6
6
|
export * from "./help.js";
|
|
7
|
+
export * from "./intents.js";
|
|
7
8
|
export * from "./nouns.js";
|
|
8
9
|
export * from "./output.js";
|
|
9
10
|
export * from "./program.js";
|
|
10
11
|
export * from "./report.js";
|
|
12
|
+
export * from "./skills.js";
|
|
11
13
|
export * from "./ui.js";
|
|
12
14
|
export * from "./theme.js";
|
package/dist/index.js
CHANGED
|
@@ -4,9 +4,11 @@ export * from "./color.js";
|
|
|
4
4
|
export * from "./confirm.js";
|
|
5
5
|
export * from "./errors.js";
|
|
6
6
|
export * from "./help.js";
|
|
7
|
+
export * from "./intents.js";
|
|
7
8
|
export * from "./nouns.js";
|
|
8
9
|
export * from "./output.js";
|
|
9
10
|
export * from "./program.js";
|
|
10
11
|
export * from "./report.js";
|
|
12
|
+
export * from "./skills.js";
|
|
11
13
|
export * from "./ui.js";
|
|
12
14
|
export * from "./theme.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { type NounSpec } from "./nouns.js";
|
|
3
|
+
/**
|
|
4
|
+
* One thing a person asks for, mapped to the CLI command and the MCP tool that answer it.
|
|
5
|
+
* The skill, the MCP tool descriptions and the help text all render from these fields.
|
|
6
|
+
*/
|
|
7
|
+
export interface Intent {
|
|
8
|
+
/** What the person says, e.g. "my grades / what did I get". */
|
|
9
|
+
readonly when: string;
|
|
10
|
+
/** What comes back, as a sentence. */
|
|
11
|
+
readonly what: string;
|
|
12
|
+
/** The command as typed, with placeholders such as UNIT; omitted for an MCP-only tool. */
|
|
13
|
+
readonly command?: string;
|
|
14
|
+
/** The MCP tool for the same request; omitted for a CLI-only command. */
|
|
15
|
+
readonly tool?: string;
|
|
16
|
+
/** Where nearby requests go instead, e.g. "get_thread for thread bodies". */
|
|
17
|
+
readonly instead?: string;
|
|
18
|
+
/** The references and arguments it accepts. */
|
|
19
|
+
readonly refs?: string;
|
|
20
|
+
/** The usual next step. */
|
|
21
|
+
readonly then?: string;
|
|
22
|
+
/** Rough response size. */
|
|
23
|
+
readonly cost?: string;
|
|
24
|
+
}
|
|
25
|
+
/** The MCP tool description for an intent: prose, because a model reads it. */
|
|
26
|
+
export declare function toolDescription(intent: Intent): string;
|
|
27
|
+
/**
|
|
28
|
+
* Throw when an intent's command no longer parses against the program: a renamed noun or verb,
|
|
29
|
+
* a wrong binary name, or an option that was removed. Run it in the CLI's tests.
|
|
30
|
+
*/
|
|
31
|
+
export declare function checkIntentCommands(program: Command, nouns: readonly NounSpec[], intents: readonly Intent[]): void;
|
package/dist/intents.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { insertDefaultVerb } from "./nouns.js";
|
|
2
|
+
/** The MCP tool description for an intent: prose, because a model reads it. */
|
|
3
|
+
export function toolDescription(intent) {
|
|
4
|
+
return [
|
|
5
|
+
sentence(intent.what),
|
|
6
|
+
sentence(`Use when the user says "${intent.when}"`),
|
|
7
|
+
intent.instead && sentence(`Instead: ${intent.instead}`),
|
|
8
|
+
intent.refs && sentence(`Input: ${intent.refs}`),
|
|
9
|
+
intent.then && sentence(`Then: ${intent.then}`),
|
|
10
|
+
intent.cost && sentence(`Cost: ${intent.cost}`),
|
|
11
|
+
].filter(Boolean).join(" ");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Throw when an intent's command no longer parses against the program: a renamed noun or verb,
|
|
15
|
+
* a wrong binary name, or an option that was removed. Run it in the CLI's tests.
|
|
16
|
+
*/
|
|
17
|
+
export function checkIntentCommands(program, nouns, intents) {
|
|
18
|
+
const problems = intents.flatMap((intent) => (intent.command ? commandProblems(program, nouns, intent.command) : []));
|
|
19
|
+
if (problems.length)
|
|
20
|
+
throw new Error(`Intent commands out of date:\n${problems.join("\n")}`);
|
|
21
|
+
}
|
|
22
|
+
const ALWAYS_ACCEPTED = new Set(["-h", "--help", "-V", "--version"]);
|
|
23
|
+
function commandProblems(program, nouns, text) {
|
|
24
|
+
const [binary, ...rest] = words(text);
|
|
25
|
+
if (binary !== program.name())
|
|
26
|
+
return [`${text}: starts with "${binary}", not "${program.name()}"`];
|
|
27
|
+
const argv = insertDefaultVerb(rest.map(unwrapOptionalFlag), nouns);
|
|
28
|
+
const path = [program];
|
|
29
|
+
let unmatched;
|
|
30
|
+
for (const token of argv) {
|
|
31
|
+
if (token.startsWith("-"))
|
|
32
|
+
continue;
|
|
33
|
+
const current = path.at(-1);
|
|
34
|
+
const child = current.commands.find((command) => command.name() === token || command.aliases().includes(token));
|
|
35
|
+
if (!child) {
|
|
36
|
+
unmatched = token;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
path.push(child);
|
|
40
|
+
}
|
|
41
|
+
const problems = [];
|
|
42
|
+
const resolved = path.at(-1);
|
|
43
|
+
// A group that takes no arguments of its own must be followed by one of its commands.
|
|
44
|
+
if (unmatched !== undefined && resolved.commands.length && !resolved.registeredArguments.length) {
|
|
45
|
+
problems.push(`${text}: "${unmatched}" is not a command of "${path.map((command) => command.name()).join(" ")}"`);
|
|
46
|
+
}
|
|
47
|
+
const options = new Set(path.flatMap((command) => command.options.flatMap(optionNames)));
|
|
48
|
+
for (const token of argv) {
|
|
49
|
+
const flag = token.split("=")[0] ?? token;
|
|
50
|
+
if (flag.startsWith("-") && !ALWAYS_ACCEPTED.has(flag) && !options.has(flag)) {
|
|
51
|
+
problems.push(`${text}: unknown option "${flag}"`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return problems;
|
|
55
|
+
}
|
|
56
|
+
// Quoted strings and bracketed placeholders such as <lesson id> stay one word.
|
|
57
|
+
function words(text) {
|
|
58
|
+
return text.replace(/^`|`$/g, "").match(/"[^"]*"|'[^']*'|<[^>]*>|\[[^\]]*\]|\S+/g) ?? [];
|
|
59
|
+
}
|
|
60
|
+
// `[--final]` documents an optional flag; check it like `--final`.
|
|
61
|
+
function unwrapOptionalFlag(token) {
|
|
62
|
+
const inner = /^\[(-[^\s\]]+)/.exec(token);
|
|
63
|
+
return inner?.[1] ?? token;
|
|
64
|
+
}
|
|
65
|
+
function optionNames(option) {
|
|
66
|
+
return [option.long, option.short].filter((name) => Boolean(name));
|
|
67
|
+
}
|
|
68
|
+
function sentence(text) {
|
|
69
|
+
return /[.!?]$/.test(text) ? text : `${text}.`;
|
|
70
|
+
}
|
package/dist/nouns.d.ts
CHANGED
|
@@ -6,4 +6,30 @@ export interface NounSpec {
|
|
|
6
6
|
/** Option flags whose following token is a value, not a positional argument. */
|
|
7
7
|
readonly valueFlags?: readonly string[];
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Nouns every CLI names the same way. A CLI that offers one uses its spec as is, or with fewer
|
|
11
|
+
* verbs and its own valueFlags, so `moodle grades` and `edstem grades` read alike.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SHARED_NOUNS: {
|
|
14
|
+
readonly courses: {
|
|
15
|
+
readonly name: "courses";
|
|
16
|
+
readonly aliases: readonly ["units", "projects"];
|
|
17
|
+
readonly verbs: readonly ["list", "show", "get"];
|
|
18
|
+
readonly defaultByArity: {
|
|
19
|
+
readonly 0: "list";
|
|
20
|
+
readonly 1: "show";
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
readonly grades: {
|
|
24
|
+
readonly name: "grades";
|
|
25
|
+
readonly verbs: readonly ["list"];
|
|
26
|
+
readonly defaultByArity: {
|
|
27
|
+
readonly 0: "list";
|
|
28
|
+
readonly 1: "list";
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
/** The `status` of one row in `grades` output. */
|
|
33
|
+
export declare const GRADE_STATUSES: readonly ["graded", "ungraded", "unreleased", "not_submitted", "unavailable"];
|
|
34
|
+
export type GradeStatus = (typeof GRADE_STATUSES)[number];
|
|
9
35
|
export declare function insertDefaultVerb(argv: readonly string[], nouns: readonly NounSpec[]): string[];
|
package/dist/nouns.js
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nouns every CLI names the same way. A CLI that offers one uses its spec as is, or with fewer
|
|
3
|
+
* verbs and its own valueFlags, so `moodle grades` and `edstem grades` read alike.
|
|
4
|
+
*/
|
|
5
|
+
export const SHARED_NOUNS = {
|
|
6
|
+
courses: {
|
|
7
|
+
name: "courses",
|
|
8
|
+
aliases: ["units", "projects"],
|
|
9
|
+
verbs: ["list", "show", "get"],
|
|
10
|
+
defaultByArity: { 0: "list", 1: "show" },
|
|
11
|
+
},
|
|
12
|
+
grades: {
|
|
13
|
+
name: "grades",
|
|
14
|
+
verbs: ["list"],
|
|
15
|
+
defaultByArity: { 0: "list", 1: "list" },
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
/** The `status` of one row in `grades` output. */
|
|
19
|
+
export const GRADE_STATUSES = ["graded", "ungraded", "unreleased", "not_submitted", "unavailable"];
|
|
1
20
|
const GLOBAL_VALUE_FLAGS = new Set(["--fields", "-o", "--output"]);
|
|
2
21
|
export function insertDefaultVerb(argv, nouns) {
|
|
3
22
|
const result = [...argv];
|
package/dist/program.d.ts
CHANGED
|
@@ -37,4 +37,6 @@ export declare function createProgram(meta: {
|
|
|
37
37
|
description: string;
|
|
38
38
|
}): Command;
|
|
39
39
|
export declare function mutating(command: Command): Command;
|
|
40
|
+
/** Whether `mutating` marked this command, for a CLI that describes its own command tree. */
|
|
41
|
+
export declare function isMutating(command: Command): boolean;
|
|
40
42
|
export declare function commandsJson(program: Command): ProgramDescription;
|
package/dist/program.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { colorEnabled } from "./color.js";
|
|
3
3
|
import { styledHelp } from "./help.js";
|
|
4
|
+
import { SHARED_NOUNS } from "./nouns.js";
|
|
4
5
|
export const VERBS = ["list", "show", "read", "get", "search", "send", "submit", "set", "mark-read"];
|
|
5
6
|
const VERB_SET = new Set(VERBS);
|
|
7
|
+
const SHARED = Object.values(SHARED_NOUNS);
|
|
6
8
|
const mutations = new WeakSet();
|
|
7
9
|
const ACTION_GROUPS = new Set(["auth", "skills"]);
|
|
8
10
|
const ACTIONS = {
|
|
@@ -39,6 +41,10 @@ export function mutating(command) {
|
|
|
39
41
|
mutations.add(command);
|
|
40
42
|
return command;
|
|
41
43
|
}
|
|
44
|
+
/** Whether `mutating` marked this command, for a CLI that describes its own command tree. */
|
|
45
|
+
export function isMutating(command) {
|
|
46
|
+
return mutations.has(command);
|
|
47
|
+
}
|
|
42
48
|
export function commandsJson(program) {
|
|
43
49
|
return {
|
|
44
50
|
name: program.name(),
|
|
@@ -53,9 +59,20 @@ function describe(command, noun) {
|
|
|
53
59
|
if (noun && ACTION_GROUPS.has(noun) && !ACTIONS[noun]?.has(command.name())) {
|
|
54
60
|
throw new Error(`Unsupported action "${command.name()}" on group "${noun}".`);
|
|
55
61
|
}
|
|
62
|
+
// A shared noun's name and aliases belong to that noun alone, whether used as a name or an alias.
|
|
63
|
+
for (const word of noun ? [] : [currentNoun, ...command.aliases()]) {
|
|
64
|
+
const owner = SHARED.find((spec) => spec.name === word || spec.aliases?.includes(word));
|
|
65
|
+
if (owner && owner.name !== currentNoun) {
|
|
66
|
+
throw new Error(`"${word}" belongs to the shared noun "${owner.name}"; name that command "${owner.name}".`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
56
69
|
if (verb && !VERB_SET.has(verb)) {
|
|
57
70
|
throw new Error(`Unsupported verb "${verb}" on noun "${noun}".`);
|
|
58
71
|
}
|
|
72
|
+
const shared = SHARED.find((spec) => spec.name === noun);
|
|
73
|
+
if (verb && shared && !shared.verbs.includes(verb)) {
|
|
74
|
+
throw new Error(`Unsupported verb "${verb}" on shared noun "${noun}".`);
|
|
75
|
+
}
|
|
59
76
|
return {
|
|
60
77
|
name: command.name(),
|
|
61
78
|
noun: currentNoun,
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Intent } from "./intents.js";
|
|
2
|
+
import type { ProgramDescription } from "./program.js";
|
|
3
|
+
export interface SkillOptions {
|
|
4
|
+
/** The skill and plugin name, e.g. "edstem-cli". */
|
|
5
|
+
readonly name: string;
|
|
6
|
+
/** What the skill does, in the third person. It is also the plugin's description. */
|
|
7
|
+
readonly description: string;
|
|
8
|
+
/**
|
|
9
|
+
* When an agent should load the skill, rendered after the description as "Use when …". Name the
|
|
10
|
+
* site and quote what people say; agents undertrigger, so list more phrases rather than fewer.
|
|
11
|
+
*/
|
|
12
|
+
readonly useWhen: string;
|
|
13
|
+
readonly title: string;
|
|
14
|
+
/** Markdown between the title and the intent table: the surfaces and the reference vocabulary. */
|
|
15
|
+
readonly intro?: string;
|
|
16
|
+
readonly intents: readonly Intent[];
|
|
17
|
+
/** The MCP server name tools are qualified with: "moodle" renders `moodle:due`. Required when an intent has a tool. */
|
|
18
|
+
readonly mcpServer?: string;
|
|
19
|
+
/** Multi-step tasks rendered as checklists, after the shared one for mutating commands. */
|
|
20
|
+
readonly workflows?: readonly Workflow[];
|
|
21
|
+
/** Rules for this CLI only; the shared contract rules are always included. */
|
|
22
|
+
readonly rules?: readonly string[];
|
|
23
|
+
/** Markdown for the Setup section. */
|
|
24
|
+
readonly setup?: string;
|
|
25
|
+
/** `commandsJson(program)` output. */
|
|
26
|
+
readonly program: ProgramDescription;
|
|
27
|
+
}
|
|
28
|
+
export interface Workflow {
|
|
29
|
+
/** An imperative title, e.g. "Submit an assignment". */
|
|
30
|
+
readonly title: string;
|
|
31
|
+
/** In order; end with a step that checks the result. */
|
|
32
|
+
readonly steps: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
/** A local stdio server, or a hosted Streamable HTTP server. */
|
|
35
|
+
export type McpServer = {
|
|
36
|
+
readonly command: string;
|
|
37
|
+
readonly args?: readonly string[];
|
|
38
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
39
|
+
} | {
|
|
40
|
+
readonly type: "http";
|
|
41
|
+
readonly url: string;
|
|
42
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
43
|
+
};
|
|
44
|
+
export interface AgentFilesOptions {
|
|
45
|
+
readonly skill: SkillOptions;
|
|
46
|
+
readonly version: string;
|
|
47
|
+
/** The GitHub repository URL; it is also the plugin's homepage. */
|
|
48
|
+
readonly repository: string;
|
|
49
|
+
readonly author: {
|
|
50
|
+
readonly name: string;
|
|
51
|
+
readonly url?: string;
|
|
52
|
+
};
|
|
53
|
+
readonly license?: string;
|
|
54
|
+
readonly keywords?: readonly string[];
|
|
55
|
+
/** MCP servers the Claude Code plugin starts, keyed by server name. */
|
|
56
|
+
readonly mcpServers?: Readonly<Record<string, McpServer>>;
|
|
57
|
+
/** Codex skill metadata, written to agents/openai.yaml in the skill folder. */
|
|
58
|
+
readonly openai?: {
|
|
59
|
+
readonly displayName: string;
|
|
60
|
+
readonly shortDescription: string;
|
|
61
|
+
readonly defaultPrompt: string;
|
|
62
|
+
};
|
|
63
|
+
/** Hand-written reference pages by file name, written next to the command reference. */
|
|
64
|
+
readonly references?: Readonly<Record<string, string>>;
|
|
65
|
+
}
|
|
66
|
+
/** Where agentFiles puts the full command tree, relative to the skill folder; SKILL.md links to it. */
|
|
67
|
+
export declare const COMMAND_REFERENCE = "references/commands.md";
|
|
68
|
+
/** The frontmatter description: what the skill does, then when to use it. */
|
|
69
|
+
export declare function skillDescription(options: Pick<SkillOptions, "description" | "useWhen">): string;
|
|
70
|
+
/**
|
|
71
|
+
* SKILL.md for one CLI: what people say, what to run, the workflows, the rules and the setup.
|
|
72
|
+
* An agent loads it whole, so the command tree lives in COMMAND_REFERENCE and each MCP tool's
|
|
73
|
+
* full description stays with the server. Throws when the name or description breaks the
|
|
74
|
+
* Agent Skills limits.
|
|
75
|
+
*/
|
|
76
|
+
export declare function renderSkill(options: SkillOptions): string;
|
|
77
|
+
/** The full command tree and the exit codes, written to COMMAND_REFERENCE. */
|
|
78
|
+
export declare function renderCommandReference(program: ProgramDescription): string;
|
|
79
|
+
/**
|
|
80
|
+
* Every agent packaging file for one CLI repository, keyed by path from the repository root:
|
|
81
|
+
* the skill folder skills/<name>/ (SKILL.md, its references and Codex's agents/openai.yaml), which
|
|
82
|
+
* `npx skills add` and Claude Code both find, plus the Claude Code plugin and its single-plugin
|
|
83
|
+
* marketplace.
|
|
84
|
+
*/
|
|
85
|
+
export declare function agentFiles(options: AgentFilesOptions): Record<string, string>;
|
|
86
|
+
/**
|
|
87
|
+
* Write generated files under root. With `check`, write nothing and return the paths whose
|
|
88
|
+
* content differs, so CI can fail on a stale commit.
|
|
89
|
+
*/
|
|
90
|
+
export declare function writeAgentFiles(root: string, files: Readonly<Record<string, string>>, options?: {
|
|
91
|
+
check?: boolean;
|
|
92
|
+
}): string[];
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { stringify } from "yaml";
|
|
4
|
+
import { EXIT_CODES } from "./errors.js";
|
|
5
|
+
/** Where agentFiles puts the full command tree, relative to the skill folder; SKILL.md links to it. */
|
|
6
|
+
export const COMMAND_REFERENCE = "references/commands.md";
|
|
7
|
+
const RULES = {
|
|
8
|
+
narrow: "Start from the table row that matches the request: the narrowest command or tool answers sooner and costs fewer tokens than a broad listing.",
|
|
9
|
+
output: "Piped output is JSON. `--fields a,b` keeps only those top-level fields, which keeps long lists cheap.",
|
|
10
|
+
read: "`read` prints Markdown and changes nothing upstream, so it is safe to run unasked.",
|
|
11
|
+
mutate: "Change something upstream only when the user asked for that change: other people see it, and most changes cannot be undone.",
|
|
12
|
+
secrets: "Keep tokens, cookies and session files out of replies, logs and files; each one is a live sign-in.",
|
|
13
|
+
errors: "A failure prints one error with a `hint` on stderr; follow the hint before retrying.",
|
|
14
|
+
};
|
|
15
|
+
const MUTATION_WORKFLOW = {
|
|
16
|
+
title: "Change something upstream",
|
|
17
|
+
steps: [
|
|
18
|
+
"Check the user asked for this change, on these targets",
|
|
19
|
+
"Run the command with `--dry-run` and show the plan",
|
|
20
|
+
"Get the user's yes to that plan",
|
|
21
|
+
"Run it again with `--yes`",
|
|
22
|
+
"Read the result back with the matching `list` or `show` and report what changed",
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
/** The frontmatter description: what the skill does, then when to use it. */
|
|
26
|
+
export function skillDescription(options) {
|
|
27
|
+
return `${sentence(options.description)} Use when ${sentence(options.useWhen)}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* SKILL.md for one CLI: what people say, what to run, the workflows, the rules and the setup.
|
|
31
|
+
* An agent loads it whole, so the command tree lives in COMMAND_REFERENCE and each MCP tool's
|
|
32
|
+
* full description stays with the server. Throws when the name or description breaks the
|
|
33
|
+
* Agent Skills limits.
|
|
34
|
+
*/
|
|
35
|
+
export function renderSkill(options) {
|
|
36
|
+
const description = skillDescription(options);
|
|
37
|
+
const problems = frontmatterProblems(options.name, description);
|
|
38
|
+
if (problems.length)
|
|
39
|
+
throw new Error(`Skill "${options.name}" frontmatter:\n${problems.join("\n")}`);
|
|
40
|
+
const binary = options.program.name;
|
|
41
|
+
const hasRead = someCommand(options.program, (command) => command.verb === "read");
|
|
42
|
+
const hasMutation = someCommand(options.program, (command) => command.mutating);
|
|
43
|
+
const workflows = [...(hasMutation ? [MUTATION_WORKFLOW] : []), ...(options.workflows ?? [])];
|
|
44
|
+
return [
|
|
45
|
+
stringifyFrontmatter({ name: options.name, description }),
|
|
46
|
+
`# ${options.title}`,
|
|
47
|
+
options.intro,
|
|
48
|
+
"## What the user says, and what to run",
|
|
49
|
+
intentTable(options),
|
|
50
|
+
workflows.length
|
|
51
|
+
? ["## Workflows", "Copy the checklist into your reply and tick each step as it is done; the last step is what shows the change landed.", ...workflows.map(renderWorkflow)].join("\n\n")
|
|
52
|
+
: undefined,
|
|
53
|
+
"## Rules",
|
|
54
|
+
[
|
|
55
|
+
RULES.narrow,
|
|
56
|
+
RULES.output,
|
|
57
|
+
...(hasRead ? [RULES.read] : []),
|
|
58
|
+
...(hasMutation ? [RULES.mutate] : []),
|
|
59
|
+
RULES.secrets,
|
|
60
|
+
RULES.errors,
|
|
61
|
+
...(options.rules ?? []),
|
|
62
|
+
`Every command, argument, option and exit code is in [${COMMAND_REFERENCE}](${COMMAND_REFERENCE}); \`${binary} commands --json\` is the source of truth.`,
|
|
63
|
+
]
|
|
64
|
+
.map((rule) => `- ${rule}`)
|
|
65
|
+
.join("\n"),
|
|
66
|
+
options.setup ? `## Setup\n\n${options.setup}` : undefined,
|
|
67
|
+
].filter((section) => Boolean(section)).join("\n\n") + "\n";
|
|
68
|
+
}
|
|
69
|
+
/** The full command tree and the exit codes, written to COMMAND_REFERENCE. */
|
|
70
|
+
export function renderCommandReference(program) {
|
|
71
|
+
const exitCodes = Object.entries(EXIT_CODES).map(([code, exit]) => [String(exit), code]);
|
|
72
|
+
return [
|
|
73
|
+
`# ${program.name} commands`,
|
|
74
|
+
`Generated from \`${program.name} commands --json\` for version ${program.version}.`,
|
|
75
|
+
markdownTable(["Command", "Description", "Arguments", "Options", "Mutating"], program.commands.flatMap((command) => commandRows(program.name, command))),
|
|
76
|
+
"## Exit codes",
|
|
77
|
+
"With `--json`, a failure is one `{ ok: false, error: { code, message, hint } }` object on stderr.",
|
|
78
|
+
markdownTable(["Exit", "Error code"], [["0", "success"], ...exitCodes]),
|
|
79
|
+
].join("\n\n") + "\n";
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Every agent packaging file for one CLI repository, keyed by path from the repository root:
|
|
83
|
+
* the skill folder skills/<name>/ (SKILL.md, its references and Codex's agents/openai.yaml), which
|
|
84
|
+
* `npx skills add` and Claude Code both find, plus the Claude Code plugin and its single-plugin
|
|
85
|
+
* marketplace.
|
|
86
|
+
*/
|
|
87
|
+
export function agentFiles(options) {
|
|
88
|
+
const { skill } = options;
|
|
89
|
+
const folder = `skills/${skill.name}`;
|
|
90
|
+
const files = {
|
|
91
|
+
[`${folder}/SKILL.md`]: renderSkill(skill),
|
|
92
|
+
[`${folder}/${COMMAND_REFERENCE}`]: renderCommandReference(skill.program),
|
|
93
|
+
};
|
|
94
|
+
for (const [name, content] of Object.entries(options.references ?? {})) {
|
|
95
|
+
files[`${folder}/${dirname(COMMAND_REFERENCE)}/${name}`] = content;
|
|
96
|
+
}
|
|
97
|
+
if (options.openai) {
|
|
98
|
+
files[`${folder}/agents/openai.yaml`] = stringify({
|
|
99
|
+
interface: {
|
|
100
|
+
display_name: options.openai.displayName,
|
|
101
|
+
short_description: options.openai.shortDescription,
|
|
102
|
+
default_prompt: options.openai.defaultPrompt,
|
|
103
|
+
},
|
|
104
|
+
}, { lineWidth: 0 });
|
|
105
|
+
}
|
|
106
|
+
// The repository root is the plugin root; Claude Code loads skills/ from it by default.
|
|
107
|
+
files[".claude-plugin/plugin.json"] = json({
|
|
108
|
+
name: skill.name,
|
|
109
|
+
version: options.version,
|
|
110
|
+
description: skill.description,
|
|
111
|
+
author: options.author,
|
|
112
|
+
homepage: options.repository,
|
|
113
|
+
repository: options.repository,
|
|
114
|
+
...(options.license ? { license: options.license } : {}),
|
|
115
|
+
...(options.keywords?.length ? { keywords: options.keywords } : {}),
|
|
116
|
+
...(options.mcpServers ? { mcpServers: options.mcpServers } : {}),
|
|
117
|
+
});
|
|
118
|
+
files[".claude-plugin/marketplace.json"] = json({
|
|
119
|
+
name: skill.name,
|
|
120
|
+
description: skill.description,
|
|
121
|
+
owner: options.author,
|
|
122
|
+
plugins: [{ name: skill.name, source: "./", description: skill.description }],
|
|
123
|
+
});
|
|
124
|
+
return files;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Write generated files under root. With `check`, write nothing and return the paths whose
|
|
128
|
+
* content differs, so CI can fail on a stale commit.
|
|
129
|
+
*/
|
|
130
|
+
export function writeAgentFiles(root, files, options = {}) {
|
|
131
|
+
const stale = Object.entries(files)
|
|
132
|
+
.filter(([path, content]) => readOrEmpty(join(root, path)) !== content)
|
|
133
|
+
.map(([path]) => path);
|
|
134
|
+
if (options.check)
|
|
135
|
+
return stale;
|
|
136
|
+
for (const path of stale) {
|
|
137
|
+
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
138
|
+
writeFileSync(join(root, path), files[path], "utf8");
|
|
139
|
+
}
|
|
140
|
+
return stale;
|
|
141
|
+
}
|
|
142
|
+
// The limits from the Agent Skills spec, plus the third person: the description is read as
|
|
143
|
+
// part of the system prompt, where "I" and "you" point at the wrong speaker.
|
|
144
|
+
function frontmatterProblems(name, description) {
|
|
145
|
+
const problems = [];
|
|
146
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) || name.length > 64)
|
|
147
|
+
problems.push(`name must be lowercase words joined by hyphens, at most 64 characters`);
|
|
148
|
+
if (/anthropic|claude/.test(name))
|
|
149
|
+
problems.push(`name must not contain "anthropic" or "claude"`);
|
|
150
|
+
if (description.length > 1024)
|
|
151
|
+
problems.push(`description is ${description.length} characters; the limit is 1024`);
|
|
152
|
+
if (/[<>]/.test(description))
|
|
153
|
+
problems.push("description must not contain < or >");
|
|
154
|
+
// Quoted phrases are what the user says, so "my grades" is fine there.
|
|
155
|
+
const unquoted = description.replace(/"[^"]*"|“[^”]*”/g, "");
|
|
156
|
+
const person = /\b(?:I|me|my|we|our|you|your)\b/i.exec(unquoted);
|
|
157
|
+
if (person)
|
|
158
|
+
problems.push(`description must be in the third person outside quotes; found "${person[0]}"`);
|
|
159
|
+
return problems;
|
|
160
|
+
}
|
|
161
|
+
function intentTable(options) {
|
|
162
|
+
const { intents, mcpServer } = options;
|
|
163
|
+
if (!mcpServer && intents.some((intent) => intent.tool)) {
|
|
164
|
+
throw new Error(`Skill "${options.name}" has MCP tools but no mcpServer to qualify them with.`);
|
|
165
|
+
}
|
|
166
|
+
const columns = [["The user says", (intent) => intent.when]];
|
|
167
|
+
if (intents.some((intent) => intent.command))
|
|
168
|
+
columns.push(["CLI", (intent) => (intent.command ? `\`${intent.command}\`` : "")]);
|
|
169
|
+
if (intents.some((intent) => intent.tool))
|
|
170
|
+
columns.push(["MCP tool", (intent) => (intent.tool ? `\`${mcpServer}:${intent.tool}\`` : "")]);
|
|
171
|
+
if (intents.some((intent) => intent.then))
|
|
172
|
+
columns.push(["Then", (intent) => intent.then ?? ""]);
|
|
173
|
+
return markdownTable(columns.map(([header]) => header), intents.map((intent) => columns.map(([, cell]) => cell(intent))));
|
|
174
|
+
}
|
|
175
|
+
// A fenced block, so the agent copies it verbatim and ticks it in its reply.
|
|
176
|
+
function renderWorkflow(workflow) {
|
|
177
|
+
return [`### ${workflow.title}`, ["```text", ...workflow.steps.map((step) => `- [ ] ${step}`), "```"].join("\n")].join("\n\n");
|
|
178
|
+
}
|
|
179
|
+
function someCommand(program, test) {
|
|
180
|
+
return program.commands.some(function visit(command) {
|
|
181
|
+
return test(command) || command.commands.some(visit);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function commandRows(binary, command, parents = []) {
|
|
185
|
+
const path = [...parents, command.name];
|
|
186
|
+
const row = [
|
|
187
|
+
`\`${[binary, ...path].join(" ")}\``,
|
|
188
|
+
command.description,
|
|
189
|
+
command.positionals.map((argument) => {
|
|
190
|
+
const name = argument.variadic ? `${argument.name}...` : argument.name;
|
|
191
|
+
return argument.required ? `<${name}>` : `[${name}]`;
|
|
192
|
+
}).join(" "),
|
|
193
|
+
command.options.map((option) => option.flags).join("<br>"),
|
|
194
|
+
command.mutating ? "yes" : "no",
|
|
195
|
+
];
|
|
196
|
+
return [row, ...command.commands.flatMap((child) => commandRows(binary, child, path))];
|
|
197
|
+
}
|
|
198
|
+
function stringifyFrontmatter(fields) {
|
|
199
|
+
return `---\n${stringify(fields, { lineWidth: 0 })}---`;
|
|
200
|
+
}
|
|
201
|
+
function markdownTable(headers, rows) {
|
|
202
|
+
return [
|
|
203
|
+
`| ${headers.join(" | ")} |`,
|
|
204
|
+
`| ${headers.map(() => "---").join(" | ")} |`,
|
|
205
|
+
...rows.map((row) => `| ${row.map(escapeCell).join(" | ")} |`),
|
|
206
|
+
].join("\n");
|
|
207
|
+
}
|
|
208
|
+
function escapeCell(value) {
|
|
209
|
+
return value.replace(/\|/g, "\\|").replace(/\n/g, "<br>");
|
|
210
|
+
}
|
|
211
|
+
function sentence(text) {
|
|
212
|
+
const trimmed = text.trim();
|
|
213
|
+
return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
|
|
214
|
+
}
|
|
215
|
+
function json(value) {
|
|
216
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
217
|
+
}
|
|
218
|
+
function readOrEmpty(path) {
|
|
219
|
+
try {
|
|
220
|
+
return readFileSync(path, "utf8");
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return "";
|
|
224
|
+
}
|
|
225
|
+
}
|
package/dist/theme.js
CHANGED
|
@@ -10,8 +10,8 @@ const TONE_STYLE = {
|
|
|
10
10
|
// Checked in order: a negated word ("not graded") must win over the word it contains.
|
|
11
11
|
const TONE_WORDS = [
|
|
12
12
|
["muted", /^(?:-|—|none|n\/a|unknown)$|\b(?:not[ _](?:started|submitted|graded|attempted|available|yet)|no submission|un(?:attempted|graded|submitted)|archived|hidden|unlisted|inactive|closed|disabled)\b/iu],
|
|
13
|
-
["danger", /\b(?:overdue|late|fail(?:ed|ure)?|error|missing|rejected|need(?:s)?[ _]help|blocked|expired|redo|resubmit|demonstrate|exceeded)\b/iu],
|
|
14
|
-
["warning", /\b(?:pending|draft|working|in[ _]progress|due soon|today|tomorrow|awaiting|unanswered|scheduled|discuss|attempted|partial)\b/iu],
|
|
13
|
+
["danger", /\b(?:overdue|late|fail(?:ed|ure)?|error|missing|rejected|need(?:s)?[ _]help|blocked|expired|redo|resubmit|demonstrate|exceeded|unavailable)\b/iu],
|
|
14
|
+
["warning", /\b(?:pending|draft|working|in[ _]progress|due soon|today|tomorrow|awaiting|unanswered|scheduled|discuss|attempted|partial|unreleased)\b/iu],
|
|
15
15
|
["success", /\b(?:complete[d]?|done|submitted|graded|accepted|passed|resolved|answered|ready[ _]for[ _]feedback|active|signed in|ok|success(?:ful)?|enrolled)\b/iu],
|
|
16
16
|
["accent", /\b(?:announcement|pinned|staff|endorsed|official|urgent)\b/iu],
|
|
17
17
|
["info", /\b(?:question|post|note|general|info)\b/iu],
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bunizao/cli-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Shared command-line contract for Bunizao CLI tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./intents": {
|
|
12
|
+
"types": "./dist/intents.d.ts",
|
|
13
|
+
"import": "./dist/intents.js"
|
|
10
14
|
}
|
|
11
15
|
},
|
|
12
16
|
"files": [
|