Skip to main content

agentic-cli-npm-package

Scaffold and ship a companion tool for agentic coding CLIs (Claude Code, Codex, Gemini, and others) as one package with three faces — a CLI, an npm library, and an MCP server — sharing a model-preference cascade core, and auto-publishing to npm on a release branch. Use when building a Claude-Code/Codex companion, a `vibe*`-style tool, or any CLI+npm+MCP package that should work with the user's own keys and fall back to on-device. Fills the gap between build-from-template (generic scaffold) and ship-a-product (launch orchestration) for the specific CLI+npm+MCP+cascade shape. Empirical, from shipping 7 packages in one run.

Source facts

Repository
pooriaarab/skills
Last source activity
October 11, 2026 at 05:26
Detected SKILL.md language
English
Stars
1
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
agentic-cli-npm-package
description
Scaffold and ship a companion tool for agentic coding CLIs (Claude Code, Codex, Gemini, and others) as one package with three faces — a CLI, an npm library, and an MCP server — sharing a model-preference cascade core, and auto-publishing to npm on a release branch. Use when building a Claude-Code/Codex companion, a `vibe*`-style tool, or any CLI+npm+MCP package that should work with the user's own keys and fall back to on-device. Fills the gap between build-from-template (generic scaffold) and ship-a-product (launch orchestration) for the specific CLI+npm+MCP+cascade shape. Empirical, from shipping 7 packages in one run.
# agentic-cli-npm-package Three faces, one engine. A companion for agentic coding CLIs ships as a **CLI** (`npx tool`), an **npm library** (programmatic API), and an **MCP server** (so the agent itself can drive it). All three are thin wrappers over one product engine, which sits on a shared **cascade core**. Chains after `build-from-template`, before `ship-a-product`. ## 1. The shape ``` package/ src/index.ts library API (pure where possible — the testable core) src/cli.ts #!/usr/bin/env node — arg parse → calls index src/mcp.ts @modelcontextprotocol/sdk stdio server → calls index src/*.test.ts vitest; the pure logic is where the real tests live .github/workflows/publish.yml ``` `package.json` essentials: - `"bin": { "tool": "./dist/cli.js" }` — the installed command stays bare (`tool`) even when the package is scoped (`@scope/tool`). Scope the package, not the command. - `"build": "tsup src/index.ts src/cli.ts src/mcp.ts --format esm --dts --clean"` — three entry points, one build. - `"files": ["dist"]`, `"publishConfig": { "access": "public" }`, `"type": "module"`, `engines.node >=18`. - Deps: the cascade core + `@modelcontextprotocol/sdk`. Add `@types/node` to devDeps **from the start** — a CLI touches `process`/`setInterval`/`node:*` and typecheck fails without it. Set `"types": ["node"]` in tsconfig. ## 2. The model-preference cascade (why these tools feel good) Don't hardcode a provider. A product declares a *capability* (`audio`, `video`, `usage-read`, …); the core resolves it in a fixed order: 1. **reuse the agent's existing provider** if it already has one for that capability, 2. **a key the user brings** for that capability, 3. **on-device / local** — works offline, no key, the guaranteed floor. Ship v0 on **tier 3** (local): it works for everyone with zero setup, and you add the paid tiers behind the same call later. Gate tiers 1–2 behind a consent record so "no data out" is enforceable, not a slogan. ## 3. Scaffold first, then delegate the src The scaffold (package.json, tsconfig, workflow, LICENSE, .gitignore) is judgment work — do it yourself once, then reuse it verbatim across sibling packages. Only `src/` + README is worth delegating to a worker. Give the worker: the capability to build on, the exact `src/index.ts`/`cli.ts`/`mcp.ts` responsibilities, a "definition of done" (`npm run build && typecheck && test` all green), and "touch only src/ + README". Verify every worker diff yourself: build, typecheck, test, **and run the real CLI** (tests passing ≠ the binary works). ## 4. Publish pipeline: main = staging, release = production `publish.yml` triggers on push to `release`, guards on version, publishes: ```yaml name: publish on: { push: { branches: [release] }, workflow_dispatch: {} } jobs: publish: runs-on: ${{ github.event.repository.private && 'boat' || 'ubuntu-latest' }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20', registry-url: 'https://registry.npmjs.org' } - run: npm ci - run: npm run build --if-present - run: npm test --if-present - name: Publish if version is new run: | NAME=$(node -p "require('./package.json').name") VER=$(node -p "require('./package.json').version") npm view "$NAME@$VER" version >/dev/null 2>&1 \ && echo "already published — skipping" \ || npm publish --access public env: { NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}" } ``` Set the `NPM_TOKEN` secret once per repo (`gh secret set NPM_TOKEN --repo owner/name`). The version guard makes re-runs idempotent (no "cannot publish over existing version" failures). Keep the guard when the job grows more steps. If the job signs an extension, uploads to a store, or tags after `npm publish`, a failure there leaves the version on npm. The guard is what lets `gh run rerun <id> --failed` skip the publish and reach the failed step. Without it, every re-run stops at npm's "cannot publish over" error. ## 5. Naming: check npm before you commit to a name Bare names are usually taken. Check `npm view <name>` and `npm view @scope/<name>` up front. If taken by someone else, publish under **your own user scope** (`@you/name`) — always available, collision-free, and the `bin` keeps the command bare. Check package-name availability the same session you check the domain. **A free name can still be refused.** `npm view <name>` answering 404 does not mean npm accepts it. The first `npm publish` can fail with E403 "too similar to an existing package". Three of 19 names in one run failed this way: `foxden` (like `boxen`), `foxlink` (like `oxlint`), and `foxpay` (like `fox-pay`). The refusal comes at release time, after the README, the imports, and the docs already use the name. - Treat a name one or two letters away from a popular package as at risk. There is no dry-run check for this. Pick a name with a real suffix from the start, or ship the first version early to find out. - A scoped name (`@you/foxden`) is not checked for similarity, so you can keep the bare name under your scope. - Or keep the repo name and publish under a plain name that contains it: `foxden-sandbox`, `foxlink-oauth`, `foxpay-agent`. A hyphen-only variant (`fox-den`) does not help (see section 6). - A rename after a scoped publish leaves the old `@you/name` copy on npm. `npm deprecate` it to point at the new name. ## 6. Gotchas (each cost a cycle) - **CRITICAL — a tsup-built ESM CLI silently no-ops under `npx` / global install.** The standard main-module guard `if (import.meta.url === pathToFileURL(process.argv[1]).href) main()` works via `node dist/cli.js` but exits 0 with zero output when run through the installed bin: npm links the bin as a **symlink**, so `process.argv[1]` is the symlink path while `import.meta.url` resolves to the real path — they never compare equal and `main()` never runs. Fix: resolve the symlink first — `import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href` (`realpathSync` from `node:fs`). Do NOT "fix" it by calling `main()` unconditionally — that runs `main()` when tests import `cli.ts`, and its `process.exit` crashes vitest ("process.exit unexpectedly called"). After the change: run the tests before republishing, and verify the real failure mode by symlinking `dist/cli.js` into `/tmp` and executing the symlink — plain `node dist/cli.js` never reproduces it. - **Registry read-side lags the publish** by minutes on a brand-new scope. The Actions log showing `+ @scope/name@x.y.z` means it published; a 404 right after is propagation, not failure. Poll the registry, don't re-publish. - **A pre-push guard that blocks pushing to `main`** evaluates the *current* branch before your command runs — `git checkout -b feat` and `git push` in one shell line still trips it. Branch in one step, push in the next. Create the long-lived `release` branch via the host API (`gh api .../git/refs`), not a local push. - **Scoped packages need `--access public`** or they publish private and fail on a free account. - **A worker that writes the files but never commits** (some headless CLIs don't exit cleanly): if the output builds + tests green, take over — commit it yourself; don't wait on a hung process. - **npm typosquat protection blocks hyphen-only variants** (403 "too similar to existing package"). If the bare name is taken, a mere hyphen variant (`viberadio` → `vibe-radio`) is still refused — the registry normalizes both to the same string. Only a real distinguishing suffix clears it (`viberadio-fm`, `vibeshare-live`); `-cli` works too. - **`npm unpublish` requires a 2FA OTP even with an automation token** — `npm publish` does not. There is no non-interactive way to delete a published package; `npm deprecate <name>@<version> "<message>"` is the token-only fallback. - **Reading `package.json` in `tsup.config.ts` via an import assertion breaks across Node versions** — `import pkg from './package.json' assert { type: 'json' }` (and the newer `with { type: 'json' }`) is a moving target; Node 25 rejects the old spelling. Use `JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'))` in the config — no syntax churn. (Verified on adscapi, Node 25.) - **Baking the version via a tsup `define` needs a non-`await` fallback for vitest.** `define: { __X_VERSION__: JSON.stringify(pkg.version) }` replaces the token at build, but under vitest (no build) the token is undefined. Resolve it as `typeof __X_VERSION__ !== 'undefined' ? __X_VERSION__ : createRequire(import.meta.url)('../package.json').version` — `typeof` on an undeclared identifier is safe (no ReferenceError), and staying synchronous avoids a top-level `await` in a library entry (which some downstream bundlers choke on). - **Give the MCP bin its OWN entry file** (`src/mcp-bin.ts` → `import { startMcp } from './mcp'; startMcp()`), separate from `src/mcp.ts`. tsup code-splitting rewrites module URLs, so the symlink main-guard above is fragile for the MCP bin; a dedicated entry that just calls `startMcp()` sidesteps the guard entirely. Point the `adscapi-mcp` bin at `dist/mcp-bin.js`. - **For an SDK that fans out to many destinations, adapters must THROW, not swallow.** If you extract adapters from a fire-and-forget backend (which logs + swallows so a failed pixel never breaks the request), flip them: throw on non-2xx so the SDK's client layer can retry + report a per-destination result. A swallowing adapter makes retries and `{ platform, ok, error }[]` results impossible. (adscapi extraction, from replytosocial.) ## 6b. Sibling packages and pnpm 11 When several of your packages depend on each other and ship in one run: - **Develop against `file:` paths.** pnpm writes a `file:` dependency to the lockfile as a path relative to the project. A worktree in `/tmp` then points the lockfile at a path that does not exist on any other machine or in CI. Keep the same relative layout as the main checkout for every worktree. - **Swap `file:` for the published range before you land.** Run `npm view <pkg> version`. When it prints a version, change the spec to `^<version>` and regenerate the lockfile on every branch that changed. CI has no `file:` path. - **pnpm 11.10 refused a dependency newer than its `minimumReleaseAge` setting in this run.** Check the default in your pnpm version. Your own package published an hour ago was too new. List the exact versions in `pnpm-workspace.yaml`: ```yaml minimumReleaseAgeExclude: - your-core@0.1.0 ``` Remove each entry when the version is old enough. - **pnpm 11.10 failed the install in this run when a dependency build script was not approved.** Approve the ones you need, for example `allowBuilds: { esbuild: true }` in `pnpm-workspace.yaml`. Check the setting name in the docs for your pnpm version. ## 7. Then `repo-standards` → `launch-video-generation` (the HyperFrames $0 path renders a distinct native-UI-style video per tool) → `social-launch-post`. See `ship-a-product` for the full chain.
View on GitHub