AXI-in-skill: bake an AXI CLI into a skill
This skill is a supplement to the axi skill, scoped
to one specific job: shipping an axi-sdk-js CLI inside a Claude Code skill so a single
npx skills add <owner>/<repo> delivers a ready-to-run, agent-facing tool with no install step.
Division of labor — keep them straight:
axi skill (upstream) |
axi-skills skill (this one) |
| What the CLI prints — TOON output, minimal schemas, content-first home view, structured errors, contextual disclosure |
How the CLI ships as a skill — esbuild bundle, committed .mjs, shim, SessionStart hook, SKILL.md generation, CI gate |
| Read it first / keep it open while writing command handlers |
Read it when you're packaging, building, or distributing |
If you're deciding what a command should output, that's the axi skill. If you're deciding how
the tool gets built and installed, you're in the right place.
When this pattern is worth it
The deciding question is does the tool make sense as a standalone install? A general-purpose
tool an agent would reach for in any repo does — publish it to npm and let agents run it via
npx -y mytool (the lavish-axi model). The
AXI-in-skill pattern is for tools that don't stand alone. Three cases (building on
agent-skills#7):
- The tool only makes sense alongside a skill. It's significantly more usable as part of a
skill that supplies the broader functionality or usage protocols the tool assumes. The canonical
example, specops: the CLI is a thin determinism layer (readiness, ordering, the DAG) over a
files-first methodology — it's near-meaningless without the spec/plan protocol the skill
teaches. Shipping the protocol (prose) and the tool together is the whole point.
- A skill needs helper scripts, and those scripts benefit from AXI principles. When a skill
would ship helper scripts anyway, building them as an AXI gets TOON output, structured errors,
content-first home views, and a session hook "for free" — worth it once the scripts do enough
that output quality matters.
- You want private distribution. For a tool you don't want on a public registry,
npx skills add <owner>/<private-repo> is a better distribution channel than publishing to npm
— the committed bundle means a clone just runs, no registry, no install.
If none of these hold — the tool is genuinely standalone and public — prefer npm. And if the
scripts are trivial and rarely called, a couple of plain zero-dep scripts beat any build pipeline.
The core idea: a committed, self-contained bundle
One esbuild step compiles your TypeScript CLI source into a single .mjs with every
dependency inlined (axi-sdk-js, @toon-format/toon, …), committed into the skill's
scripts/. It runs under plain node ≥ 20 on any machine that has only the skill installed —
no npm install, no node_modules, no npm publish. The repo is the skill.
Two alternatives were tried and rejected for this use case:
- Publish to npm, call via
npx -y mytool — splits the code across repos and adds a release
pipeline, and the tool is fetched from the registry on demand (needs network; no good for
private/offline/reproducible cases). Fine for a standalone public tool — the
lavish-axi skill is the public example of this
model — but overkill when the CLI lives with the skill and must run offline.
- A bootstrap wrapper that
npm installs on first call — adds per-machine first-run
friction. agent-skills#7 concluded:
the committed-bundle route works; drop the bootstrap wrapper.
Choose your repo layout
The recipe is identical; only paths change. Three real shapes:
- Single-skill repo (canonical: specops) —
CLI source in
src/cli/, one skill in skills/<name>/. Start here if you can; it shows the
pattern in isolation.
- Multi-skill repo (canonical: claude-assist) —
several skills, one shared build.
build-cli.ts and build-skill.ts each iterate a TARGETS
array; source lives in packages/*/src/axi/.
- CLI alongside a server app — CLI in
src/cli/ shares types with the server; the bundle is
committed under skills/<name>/scripts/. One repo, no cross-repo drift.
What you'll end up with
<repo>/
src/cli/ # CLI source (or packages/*/src/axi/ for multi-skill)
bin.ts # entry: await main()
cli.ts # runAxiCli() wiring + command registry
reference.ts # SINGLE SOURCE OF TRUTH: DESCRIPTION + COMMAND_GROUPS
invocation.ts # cliInvocation() — resolved path for emitted examples
skill.ts # spliceGeneratedRegions() — SKILL.md marker splicer
commands/{home,hook,...}.ts # handlers (home doubles as the hook payload)
scripts/
build-cli.ts # esbuild → committed .mjs, version-stamp, --check drift gate
build-skill.ts # splice generated regions into SKILL.md, --check
skills/<name>/
SKILL.md # hand-authored prose + machine-generated regions
scripts/
<tool> # bash shim (executable) — the documented invocation
<tool>.mjs # COMMITTED self-contained bundle (linguist-generated)
.gitattributes # marks <tool>.mjs linguist-generated
.github/workflows/check.yml # type-check + drift gate (--check) + tests
package.json # devDeps: axi-sdk-js, @toon-format/toon, esbuild (pinned)
Ready-to-adapt versions of every build/config file are in references/templates/.
The recipe
Work top to bottom; each step has a deep-dive reference and a template.
Add build deps (pinned). axi-sdk-js, @toon-format/toon, esbuild as devDependencies,
pinned to exact versions so the committed bundle's bytes are reproducible. Add npm scripts
build:cli, build:skill, and a combined check that runs both with --check.
Make reference.ts the single source of truth. Export DESCRIPTION and a
COMMAND_GROUPS array of {group, commands:[{usage, summary}]}. The home view's help, the
--help text, and the generated SKILL.md region all derive from this one structure — so the
skill doc can never drift from the implementation. See references/skill-md-generation.md.
Wire cli.ts with runAxiCli(). Register home, your commands, and a hook command.
Watch the two SDK quirks that bite everyone (flags-before-command rejected; the home view is
zero-arg) — both covered in references/build-pipeline.md and references/session-hooks.md.
Emit resolved invocation paths, never bare names. The bundle is not on PATH. Use
cliInvocation() (template: references/templates/invocation.ts) so every emitted example,
help line, and hook hint points at the real path. A bare mytool makes an agent assume it's
on PATH and the call fails. Detail in references/build-pipeline.md.
Build the bundle + shim. scripts/build-cli.ts runs esbuild, stamps the version from
git describe, and offers a --check drift gate. Commit <tool>.mjs and an executable bash
shim <tool>. Full reasoning (incl. the version-normalization trick) in
references/build-pipeline.md.
Generate the SKILL.md command reference. Mark a region with
<!-- BEGIN/END GENERATED: command-reference --> and let build-skill.ts splice it from
COMMAND_GROUPS. Prose outside the markers is never touched. See
references/skill-md-generation.md.
Add the SessionStart hook command. hook install|uninstall|status with project (default)
and global scope, built on the SDK's computeSessionStartHookUpdate. The project/global path
distinction (${CLAUDE_PROJECT_DIR} vs absolute) is the subtle part — see
references/session-hooks.md.
Split home from dashboard (if you have live data). A lean, offline-safe home
(identity + reference) for the cheap every-session global hook; a richer dashboard (live
state) for on-demand or project-scoped use. See references/home-vs-dashboard.md.
Gate drift in CI. A check.yml that runs type-check and bun run check (both
--check builds) with fetch-depth: 0 so git describe is stable. Template:
references/templates/check.yml.
Hard-won gotchas (the ones that cost hours)
These are distilled from building specops and claude-assist (and other internal AXI tools), and are enumerated
upstream in kunchenguid/axi#46. Internalize them
before you start — most are invisible until they bite.
- A
.mjs-named bundle never auto-installs hooks. The SDK infers auto-install from the bin
filename and silently no-ops on any filename containing a .. So you don't pass
hooks: false (it isn't even in the published AxiCliOptions type in 0.1.7) — you manage hooks
explicitly via your hook command and rely on this implicit no-op.
runAxiCli rejects flags before the command. mytool --dir x fails ("Flags must come
after the command"). The no-args home view is therefore strictly zero-arg — it can't take a
leading --dir. Lean on the SessionStart working directory instead, or register an explicit
home command for the flag-bearing form.
- The bundle is not on PATH. Always emit resolved paths via
cliInvocation().
- Version drift would fail
--check forever. Committing the bundle changes the git sha (and
CI may lack tags), so the embedded git describe version always differs. Normalize the version
literal out of both sides before comparing — covered in references/build-pipeline.md.
Write doesn't set +x. The shim and .mjs must be committed executable (chmod 755).
os.homedir() ignores $HOME. Don't try to fake $HOME to test global hook install — it
writes to your real ~/.claude/settings.json. Install-then-uninstall against the real path and
assert other hooks survive.
- Mark the bundle
linguist-generated in .gitattributes so the giant generated .mjs
collapses in PR diffs.
Templates
references/templates/ holds adaptable starting points — copy, then rename mytool/MyTool
to your CLI:
build-cli.ts — esbuild bundle + version stamp + --check drift gate
build-skill.ts — SKILL.md region splicer (single- and multi-skill TARGETS forms)
invocation.ts — cliInvocation() resolved-path helper
skill.ts — spliceGeneratedRegions() marker engine + COMMAND_GROUPS → markdown
shim — the three-line bash shim
gitattributes — the linguist-generated line
check.yml — the CI drift gate
1---2name: axi-skills3description: Packaging-and-distribution recipe for baking an AXI CLI (axi-sdk-js) into a Claude Code skill — bundling it under the skill's scripts/ as a self-contained committed .mjs shipped via `npx skills add`. This is the build/ship companion to the upstream `axi` skill: `axi` covers what good agent-facing CLI output looks like (TOON, schemas, content-first); this covers how to ship that CLI *inside a skill*. Use whenever you're turning an AXI tool into an installable skill, or working on any of its moving parts — the esbuild → committed .mjs build, the bash shim, the SessionStart hook (install/uninstall/status, project vs global scope), generating SKILL.md from a single-source command reference, the CI drift gate, or splitting a lean every-session "home" view from an on-demand live "dashboard". Triggers: "bundle my CLI in a skill", "AXI in a skill", "ship this CLI via npx skills", "self-contained skill bundle", "add a session-start hook to my skill", "generate SKILL.md from the CLI", or noticing a repo with `scripts/b4---56# AXI-in-skill: bake an AXI CLI into a skill78This skill is a **supplement to the [`axi`](https://github.com/kunchenguid/axi) skill**, scoped9to one specific job: shipping an `axi-sdk-js` CLI *inside* a Claude Code skill so a single10`npx skills add <owner>/<repo>` delivers a ready-to-run, agent-facing tool with no install step.1112Division of labor — keep them straight:1314| `axi` skill (upstream) | `axi-skills` skill (this one) |15|---|---|16| **What** the CLI prints — TOON output, minimal schemas, content-first home view, structured errors, contextual disclosure | **How** the CLI ships as a skill — esbuild bundle, committed `.mjs`, shim, SessionStart hook, SKILL.md generation, CI gate |17| Read it first / keep it open while writing command handlers | Read it when you're packaging, building, or distributing |1819If you're deciding what a command should *output*, that's the `axi` skill. If you're deciding how20the tool gets *built and installed*, you're in the right place.2122## When this pattern is worth it2324The deciding question is **does the tool make sense as a standalone install?** A general-purpose25tool an agent would reach for in any repo *does* — publish it to npm and let agents run it via26`npx -y mytool` (the [lavish-axi](https://github.com/kunchenguid/lavish-axi) model). The27AXI-in-skill pattern is for tools that **don't** stand alone. Three cases (building on28[agent-skills#7](https://github.com/JarvusInnovations/agent-skills/issues/7)):29301. **The tool only makes sense alongside a skill.** It's significantly more usable as part of a31 skill that supplies the broader functionality or usage protocols the tool assumes. The canonical32 example, specops: the CLI is a thin determinism layer (readiness, ordering, the DAG) over a33 files-first *methodology* — it's near-meaningless without the spec/plan protocol the skill34 teaches. Shipping the protocol (prose) and the tool together is the whole point.352. **A skill needs helper scripts, and those scripts benefit from AXI principles.** When a skill36 would ship helper scripts anyway, building them as an AXI gets TOON output, structured errors,37 content-first home views, and a session hook "for free" — worth it once the scripts do enough38 that output quality matters.393. **You want private distribution.** For a tool you don't want on a public registry,40 `npx skills add <owner>/<private-repo>` is a better distribution channel than publishing to npm41 — the committed bundle means a clone just runs, no registry, no install.4243If none of these hold — the tool is genuinely standalone and public — prefer npm. And if the44scripts are trivial and rarely called, a couple of plain zero-dep scripts beat any build pipeline.4546## The core idea: a committed, self-contained bundle4748One esbuild step compiles your TypeScript CLI source into a **single `.mjs` with every49dependency inlined** (`axi-sdk-js`, `@toon-format/toon`, …), committed into the skill's50`scripts/`. It runs under plain `node ≥ 20` on any machine that has only the skill installed —51no `npm install`, no `node_modules`, no npm publish. **The repo *is* the skill.**5253Two alternatives were tried and rejected for this use case:5455- **Publish to npm, call via `npx -y mytool`** — splits the code across repos and adds a release56 pipeline, and the tool is fetched from the registry on demand (needs network; no good for57 private/offline/reproducible cases). Fine for a standalone public tool — the58 [lavish-axi](https://github.com/kunchenguid/lavish-axi) skill is the public example of this59 model — but overkill when the CLI lives with the skill and must run offline.60- **A bootstrap wrapper that `npm install`s on first call** — adds per-machine first-run61 friction. [agent-skills#7](https://github.com/JarvusInnovations/agent-skills/issues/7) concluded:62 *the committed-bundle route works; drop the bootstrap wrapper.*6364## Choose your repo layout6566The recipe is identical; only paths change. Three real shapes:67681. **Single-skill repo** (canonical: [specops](https://github.com/JarvusInnovations/specops)) —69 CLI source in `src/cli/`, one skill in `skills/<name>/`. Start here if you can; it shows the70 pattern in isolation.712. **Multi-skill repo** (canonical: [claude-assist](https://github.com/JarvusInnovations/claude-assist)) —72 several skills, one shared build. `build-cli.ts` and `build-skill.ts` each iterate a `TARGETS`73 array; source lives in `packages/*/src/axi/`.743. **CLI alongside a server app** — CLI in `src/cli/` shares types with the server; the bundle is75 committed under `skills/<name>/scripts/`. One repo, no cross-repo drift.7677## What you'll end up with7879```80<repo>/81 src/cli/ # CLI source (or packages/*/src/axi/ for multi-skill)82 bin.ts # entry: await main()83 cli.ts # runAxiCli() wiring + command registry84 reference.ts # SINGLE SOURCE OF TRUTH: DESCRIPTION + COMMAND_GROUPS85 invocation.ts # cliInvocation() — resolved path for emitted examples86 skill.ts # spliceGeneratedRegions() — SKILL.md marker splicer87 commands/{home,hook,...}.ts # handlers (home doubles as the hook payload)88 scripts/89 build-cli.ts # esbuild → committed .mjs, version-stamp, --check drift gate90 build-skill.ts # splice generated regions into SKILL.md, --check91 skills/<name>/92 SKILL.md # hand-authored prose + machine-generated regions93 scripts/94 <tool> # bash shim (executable) — the documented invocation95 <tool>.mjs # COMMITTED self-contained bundle (linguist-generated)96 .gitattributes # marks <tool>.mjs linguist-generated97 .github/workflows/check.yml # type-check + drift gate (--check) + tests98 package.json # devDeps: axi-sdk-js, @toon-format/toon, esbuild (pinned)99```100101Ready-to-adapt versions of every build/config file are in `references/templates/`.102103## The recipe104105Work top to bottom; each step has a deep-dive reference and a template.1061071. **Add build deps (pinned).** `axi-sdk-js`, `@toon-format/toon`, `esbuild` as devDependencies,108 pinned to exact versions so the committed bundle's bytes are reproducible. Add npm scripts109 `build:cli`, `build:skill`, and a combined `check` that runs both with `--check`.1101112. **Make `reference.ts` the single source of truth.** Export `DESCRIPTION` and a112 `COMMAND_GROUPS` array of `{group, commands:[{usage, summary}]}`. The home view's help, the113 `--help` text, and the generated SKILL.md region all derive from this one structure — so the114 skill doc can never drift from the implementation. See `references/skill-md-generation.md`.1151163. **Wire `cli.ts` with `runAxiCli()`.** Register `home`, your commands, and a `hook` command.117 Watch the two SDK quirks that bite everyone (flags-before-command rejected; the home view is118 zero-arg) — both covered in `references/build-pipeline.md` and `references/session-hooks.md`.1191204. **Emit resolved invocation paths, never bare names.** The bundle is **not on `PATH`**. Use121 `cliInvocation()` (template: `references/templates/invocation.ts`) so every emitted example,122 help line, and hook hint points at the real path. A bare `mytool` makes an agent assume it's123 on PATH and the call fails. Detail in `references/build-pipeline.md`.1241255. **Build the bundle + shim.** `scripts/build-cli.ts` runs esbuild, stamps the version from126 `git describe`, and offers a `--check` drift gate. Commit `<tool>.mjs` and an executable bash127 shim `<tool>`. Full reasoning (incl. the version-normalization trick) in128 `references/build-pipeline.md`.1291306. **Generate the SKILL.md command reference.** Mark a region with131 `<!-- BEGIN/END GENERATED: command-reference -->` and let `build-skill.ts` splice it from132 `COMMAND_GROUPS`. Prose outside the markers is never touched. See133 `references/skill-md-generation.md`.1341357. **Add the SessionStart hook command.** `hook install|uninstall|status` with project (default)136 and global scope, built on the SDK's `computeSessionStartHookUpdate`. The project/global path137 distinction (`${CLAUDE_PROJECT_DIR}` vs absolute) is the subtle part — see138 `references/session-hooks.md`.1391408. **Split home from dashboard (if you have live data).** A lean, offline-safe `home`141 (identity + reference) for the cheap every-session global hook; a richer `dashboard` (live142 state) for on-demand or project-scoped use. See `references/home-vs-dashboard.md`.1431449. **Gate drift in CI.** A `check.yml` that runs `type-check` and `bun run check` (both145 `--check` builds) with `fetch-depth: 0` so `git describe` is stable. Template:146 `references/templates/check.yml`.147148## Hard-won gotchas (the ones that cost hours)149150These are distilled from building specops and claude-assist (and other internal AXI tools), and are enumerated151upstream in [kunchenguid/axi#46](https://github.com/kunchenguid/axi/issues/46). Internalize them152before you start — most are invisible until they bite.153154- **A `.mjs`-named bundle never auto-installs hooks.** The SDK infers auto-install from the bin155 filename and silently no-ops on any filename containing a `.`. So you *don't* pass156 `hooks: false` (it isn't even in the published `AxiCliOptions` type in 0.1.7) — you manage hooks157 explicitly via your `hook` command and rely on this implicit no-op.158- **`runAxiCli` rejects flags before the command.** `mytool --dir x` fails ("Flags must come159 after the command"). The no-args home view is therefore strictly zero-arg — it can't take a160 leading `--dir`. Lean on the SessionStart working directory instead, or register an explicit161 `home` command for the flag-bearing form.162- **The bundle is not on PATH.** Always emit resolved paths via `cliInvocation()`.163- **Version drift would fail `--check` forever.** Committing the bundle changes the git sha (and164 CI may lack tags), so the embedded `git describe` version always differs. Normalize the version165 literal out of *both* sides before comparing — covered in `references/build-pipeline.md`.166- **`Write` doesn't set +x.** The shim and `.mjs` must be committed executable (`chmod 755`).167- **`os.homedir()` ignores `$HOME`.** Don't try to fake `$HOME` to test global hook install — it168 writes to your real `~/.claude/settings.json`. Install-then-uninstall against the real path and169 assert other hooks survive.170- **Mark the bundle `linguist-generated`** in `.gitattributes` so the giant generated `.mjs`171 collapses in PR diffs.172173## Templates174175`references/templates/` holds adaptable starting points — copy, then rename `mytool`/`MyTool`176to your CLI:177178- `build-cli.ts` — esbuild bundle + version stamp + `--check` drift gate179- `build-skill.ts` — SKILL.md region splicer (single- and multi-skill `TARGETS` forms)180- `invocation.ts` — `cliInvocation()` resolved-path helper181- `skill.ts` — `spliceGeneratedRegions()` marker engine + `COMMAND_GROUPS` → markdown182- `shim` — the three-line bash shim183- `gitattributes` — the `linguist-generated` line184- `check.yml` — the CI drift gate