diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 012588d..86fc2da 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -276,6 +276,9 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -283,35 +286,8 @@ jobs: check-latest: true - name: Wait for npm propagation and smoke-test ai-tools - run: | - node -e " - const { spawnSync } = require('node:child_process'); - - function runAiTools() { - return spawnSync('npx', ['--yes', '@itz4blitz/ai-tools@latest', '--help'], { - encoding: 'utf-8', - shell: true, - }); - } - - const maxAttempts = 8; - for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { - const result = runAiTools(); - const output = (result.stdout || '') + (result.stderr || ''); - const ok = result.status === 0 && /ai-tools\s+-\s+Unified CLI/.test(output); - - if (ok) { - console.log('ai-tools smoke test passed'); - process.exit(0); - } - - console.log('attempt ' + attempt + '/' + maxAttempts + ' failed'); - if (attempt < maxAttempts) { - const waitMs = 15000; - Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitMs); - } - } - - console.error('ai-tools smoke test failed after retries'); - process.exit(1); - " + run: node scripts/smoke-published.mjs + env: + AI_TOOLS_SMOKE_PACKAGE: "@itz4blitz/ai-tools@latest" + AI_TOOLS_SMOKE_ATTEMPTS: "12" + AI_TOOLS_SMOKE_WAIT_MS: "15000" diff --git a/README.md b/README.md index 46ea917..3f3e8d0 100644 --- a/README.md +++ b/README.md @@ -1,157 +1,103 @@ -# ai-tools +# @itz4blitz/ai-tools [![CI](https://github.com/itz4blitz/ai-tools/actions/workflows/pipeline.yml/badge.svg)](https://github.com/itz4blitz/ai-tools/actions/workflows/pipeline.yml) [![npm](https://img.shields.io/npm/v/@itz4blitz/ai-tools.svg)](https://www.npmjs.com/package/@itz4blitz/ai-tools) ![Node.js >=22](https://img.shields.io/badge/node-%3E%3D22-339933?logo=node.js&logoColor=white) ![License MIT](https://img.shields.io/badge/license-MIT-16A34A) -**Write your AI-tool config once. `ai-tools` emits the native files each host actually reads.** +One CLI that writes the **native** config files each AI coding tool actually loads — MCP servers, skills, agents, rules, and hooks. -Hooks, MCP servers, skills, agents, and rules all have a different shape in Claude Code, Cursor, OpenCode, and Codex. This CLI is the translator — plus a plugin bundle layer so you can ship one capability pack onto the hosts installed on the machine. +Claude Code, Cursor, Codex, OpenCode, Grok, and ZCode do not share a format. Cursor wants `.cursor/mcp.json` with `"type": "http"`. Grok wants TOML `[mcp_servers]`. ZCode wants `mcp.servers` inside `~/.zcode/cli/config.json`. Skills live in `skills//SKILL.md` on the hosts that implement Agent Skills, and Cursor rules are flat `*.mdc` files. + +`ai-tools` is the translator. You describe the capability once. It emits the files the host reads. ```bash -npx @itz4blitz/ai-tools@latest detect +npm i -D @itz4blitz/ai-tools +npx ai-tools detect ``` -## Hosts +Requires Node 22+. -Primary fleet is Claude Code, Cursor, OpenCode, Codex, plus Grok and zcode next. The other adapters still ship. +## Do this, not that -| Host | Hooks | MCP | Agents | Skills | Rules | -|------|:-----:|:---:|:------:|:------:|:-----:| -| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | ✓ | ✓ | ✓ | ✓ | ✓ | -| [Cursor](https://cursor.com) | ✓ | ✓ | ✓ | ✓ | ✓ | -| [OpenCode](https://opencode.ai) | ✓ | ✓ | ✓ | ✓ | ✓ | -| [Codex](https://openai.com/codex/) | ✓ | ✓ | ✓ | ✓ | ✓ | -| Grok / grok build | ✓ | ✓ | ✓ | ✓ | ✓ | -| zcode | ✓ | ✓ | ✓ | ✓ | ✓ | -| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | ✓ | ✓ | ✓ | ✓ | ✓ | -| [Antigravity CLI](https://www.antigravity.google/product/antigravity-cli) | | | ✓ | ✓ | ✓ | -| [Kiro](https://kiro.dev) | ✓ | ✓ | ✓ | ✓ | ✓ | -| [Cline](https://cline.bot) | ✓ | ✓ | ✓ | ✓ | ✓ | -| [Amp](https://ampcode.com) | | ✓ | | ✓ | ✓ | -| [Factory Droid](https://factory.ai) | ✓ | ✓ | ✓ | ✓ | ✓ | -| [VS Code / Copilot](https://code.visualstudio.com) | ✱ | ✓ | ✓ | ✓ | ✓ | -| [Continue](https://continue.dev) | | | | ✓ | ✓ | -| [Roo Code](https://roocode.com) | | ✓ | ✓ | ✓ | ✓ | -| [Windsurf](https://windsurf.com) | | ✓ | | ✓ | ✓ | +**Install from a config file.** `ai-tools mcp install --layer=project` writes only the servers in `mcp.config.ts` that match the current repo. -✱ VS Code 1.109+ agent hooks use the Claude Code format. Grok and ZCode adapters cover hooks/MCP/agents/skills/rules. - -## 60-second path +**Do not copy one tool's live config onto every other tool.** That is how org MCP servers and interactive 1Password wrappers leak into user-global Cursor/Grok/ZCode configs. Unified `ai-tools sync` is disabled. `ai-tools mcp sync` is an alias of config-scoped install and **refuses** to run when the config selects nothing. ```bash -npm i -D @itz4blitz/ai-tools - -# What is installed on this machine? -npx ai-tools detect - -# One capability bundle → plan → native files -npx ai-tools plugins init -npx ai-tools plugins plan --tools=claude-code,cursor,opencode,codex -npx ai-tools plugins install --tools=claude-code,cursor,opencode,codex +npx ai-tools mcp audit # scan user-global MCP configs for those leaks ``` -Per-engine flow is the same everywhere: `init` → `detect` → `generate` → `install`. +## 60-second path ```bash -npx ai-tools hooks generate && npx ai-tools hooks install -npx ai-tools mcp generate && npx ai-tools mcp install -npx ai-tools skills generate && npx ai-tools skills install -npx ai-tools agents generate && npx ai-tools agents install -npx ai-tools rules generate && npx ai-tools rules install +npx ai-tools detect # what is installed on this machine +npx ai-tools mcp init # mcp.config.ts +# edit mcp.config.ts +npx ai-tools mcp install --layer=project # native files for detected hosts +npx ai-tools mcp audit ``` -Canonical mode keeps a single store under `.ai-tools/` and installs out from there: +Same shape for the other engines: ```bash -npx ai-tools init -npx ai-tools generate -npx ai-tools install -npx ai-tools status -``` - -```mermaid -graph TD - CFG["ai-*.config.ts / definePlugin()"] --> CLI["ai-tools CLI"] - - CLI --> Hooks - CLI --> MCP - CLI --> Agents - CLI --> Skills - CLI --> Rules - - subgraph fleet ["Primary"] - CC["Claude Code"] - Cursor["Cursor"] - OC["OpenCode"] - Codex["Codex"] - Grok["Grok / grok build"] - Zcode["zcode"] - end - - subgraph more ["Also"] - Gemini["Gemini CLI"] - Copilot["VS Code / Copilot"] - Cline["Cline"] - Kiro["Kiro"] - Antigravity["Antigravity CLI"] - Amp["Amp"] - Droid["Factory Droid"] - Continue["Continue"] - Roo["Roo Code"] - Windsurf["Windsurf"] - end - - Hooks --> fleet - MCP --> fleet - Agents --> fleet - Skills --> fleet - Rules --> fleet - Hooks --> more - MCP --> more - Agents --> more - Skills --> more - Rules --> more +npx ai-tools hooks init && npx ai-tools hooks install +npx ai-tools skills init && npx ai-tools skills install +npx ai-tools agents init && npx ai-tools agents install +npx ai-tools rules init && npx ai-tools rules install ``` -## Plugin bundle - -`definePlugin()` is the portable authoring surface. `plugins plan` tells you what each host can actually consume; `plugins install` writes only the supported pieces. +## MCP config ```ts -// ai-plugin.config.ts -import { definePlugin } from "@itz4blitz/ai-tools"; +// mcp.config.ts +import { defineConfig } from "@itz4blitz/ai-tools/mcp"; -export default definePlugin({ - id: "release-confidence", - name: "Release Confidence", - version: "0.1.0", - targets: { - include: ["claude-code", "cursor", "opencode", "codex"], - }, - mcpServers: [ +export default defineConfig({ + servers: [ { - id: "example-mcp", - name: "Example MCP", - transport: { type: "stdio", command: "npx", args: ["-y", "example-mcp"] }, + id: "github", + name: "GitHub", + transport: { type: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] }, + layer: "user", }, - ], - skills: [ { - id: "ship-check", - name: "Ship Check", - content: "Run the failing test first, then the smallest fix that makes it pass.", + id: "internal-wiki", + name: "Internal wiki", + transport: { type: "stdio", command: "wiki-mcp" }, + layer: "project", + whenPathContains: ["internal-wiki", "docs-site"], }, ], }); ``` -Hosts are not identical. Cursor and Codex can take a broad bundle. OpenCode has MCP plus a native plugin system. Claude Code is project-level hooks, agents, commands, and MCP. +| Field | Effect | +|-------|--------| +| `layer: "user"` | Eligible for user-global install (`--layer=user`) | +| `layer: "project"` (default) | Eligible for repo install (`--layer=project`) | +| `whenPathContains` | Server is skipped unless `process.cwd()` contains one of the fragments | + +Empty config → install/sync/generate **exit 1**. They will not write `{}` over a working host file. + +### What gets written -## Config examples +| Host | Project file | User file | +|------|----------------|-----------| +| Cursor | `.cursor/mcp.json` (`mcpServers`, HTTP/SSE include `type`) | `~/.cursor/mcp.json` | +| Claude Code | `.mcp.json` | `~/.claude.json` | +| Grok | `.grok/config.toml` `[mcp_servers]` | `~/.grok/config.toml` | +| Codex | `.codex/config.toml` `[mcp_servers]` | `~/.codex/config.toml` | +| OpenCode | `opencode.json` `mcp` | `~/.config/opencode/opencode.json` | +| ZCode | `.zcode/config.json` `mcp.servers` | `~/.zcode/cli/config.json` | -### Hooks +ZCode's leftover `.zcode/mcp.json` is **not** what the CLI reads. This package does not write it. + +## Skills, agents, rules, hooks + +Where the host implements Agent Skills, generate writes `skills//SKILL.md` (Grok, ZCode, Cursor, Claude Code, Codex, OpenCode). Hosts that only have prompt files keep those (Amp, Cline, Continue, …). + +Cursor rules are flat `.cursor/rules/.mdc`. OpenCode agents are `.opencode/agent/.md` (singular). Grok/ZCode hooks write JSON the host loads and a runner that actually executes `HookEngine` — not a stub `process.exit(0)`. ```ts // ai-hooks.config.ts @@ -172,106 +118,92 @@ export default defineConfig({ .priority(10) .build(), ], - settings: { hookTimeout: 5000, failMode: "open", logLevel: "warn" }, }); ``` -### MCP +Built-in guardrails: `block-dangerous-commands`, `scan-secrets`, `protect-sensitive-files`, `audit-shell`. + +## Plugin bundle + +`definePlugin()` is one file that can carry MCP + skills + agents + rules + hooks. `plugins plan` shows what each host can consume. `plugins install` writes only those pieces, and MCP servers still go through `layer` / `whenPathContains`. ```ts -// mcp.config.ts -import { defineConfig } from "@itz4blitz/ai-tools/mcp"; +// ai-plugin.config.ts +import { definePlugin } from "@itz4blitz/ai-tools"; -export default defineConfig({ - servers: [ - { - id: "filesystem", - name: "filesystem", - transport: { - type: "stdio", - command: "npx", - args: ["-y", "@modelcontextprotocol/server-filesystem", "./src"], - }, - layer: "project", - }, +export default definePlugin({ + id: "release-confidence", + name: "Release Confidence", + version: "0.1.0", + targets: { include: ["claude-code", "cursor", "opencode", "codex"] }, + mcpServers: [ { - id: "palamhealth", - name: "PalamHealth", - transport: { type: "stdio", command: "palamhealth-mcp" }, - layer: "project", - whenPathContains: ["PalamHealth"], + id: "example-mcp", + name: "Example MCP", + transport: { type: "stdio", command: "npx", args: ["-y", "example-mcp"] }, }, ], -}); -``` - -`ai-tools mcp install --layer=project` writes only matching servers. `ai-tools mcp sync` never copies imported servers between tools. `ai-tools mcp audit` reports PalamHealth leaks in user-global configs. - -### Rules - -```ts -// ai-rules.config.ts -import { defineRulesConfig } from "@itz4blitz/ai-tools/rules"; - -export default defineRulesConfig({ - rules: [ + skills: [ { - id: "typescript-strict", - name: "typescript-strict", - content: "Always use strict TypeScript. No `any` types.", - scope: { type: "always" }, - priority: 1, + id: "ship-check", + name: "Ship Check", + content: "Run the failing test first, then the smallest fix that makes it pass.", }, ], }); ``` -## Hooks runtime - -Express-style middleware over a 15-event model. Before-events can block; after-events are observe-only. - -| Category | Before (blockable) | After | -|----------|--------------------|-------| -| Session | `session:start` | `session:end` | -| Prompt | `prompt:submit` | `prompt:response` | -| Tool | `tool:before` | `tool:after` | -| File | `file:read`, `file:write`, `file:edit`, `file:delete` | | -| Shell | `shell:before` | `shell:after` | -| MCP | `mcp:before` | `mcp:after` | -| System | | `notification` | - -Built-in guardrails: `block-dangerous-commands`, `scan-secrets`, `protect-sensitive-files`, `audit-shell`. +```bash +npx ai-tools plugins plan --tools=claude-code,cursor,opencode,codex +npx ai-tools plugins install --tools=claude-code,cursor,opencode,codex +``` -```ts -import { HookEngine, builtinHooks } from "@itz4blitz/ai-tools/hooks"; +## Hosts -const engine = new HookEngine({ - hooks: builtinHooks, - settings: { failMode: "closed" }, -}); +| Host | Hooks | MCP | Agents | Skills | Rules | +|------|:-----:|:---:|:------:|:------:|:-----:| +| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | ✓ | ✓ | ✓ | ✓ | ✓ | +| [Cursor](https://cursor.com) | ✓ | ✓ | ✓ | ✓ | ✓ | +| [OpenCode](https://opencode.ai) | ✓ | ✓ | ✓ | ✓ | ✓ | +| [Codex](https://openai.com/codex/) | ✓ | ✓ | ✓ | ✓ | ✓ | +| Grok | ✓ | ✓ | ✓ | ✓ | ✓ | +| ZCode | ✓ | ✓ | ✓ | ✓ | ✓ | +| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | ✓ | ✓ | ✓ | ✓ | ✓ | +| [Antigravity CLI](https://www.antigravity.google/product/antigravity-cli) | | | ✓ | ✓ | ✓ | +| [Kiro](https://kiro.dev) | ✓ | ✓ | ✓ | ✓ | ✓ | +| [Cline](https://cline.bot) | ✓ | ✓ | ✓ | ✓ | ✓ | +| [Amp](https://ampcode.com) | | ✓ | | ✓ | ✓ | +| [Factory Droid](https://factory.ai) | ✓ | ✓ | ✓ | ✓ | ✓ | +| [VS Code / Copilot](https://code.visualstudio.com) | ✱ | ✓ | ✓ | ✓ | ✓ | +| [Continue](https://continue.dev) | | | | ✓ | ✓ | +| [Roo Code](https://roocode.com) | | ✓ | ✓ | ✓ | ✓ | +| [Windsurf](https://windsurf.com) | | ✓ | | ✓ | ✓ | -const result = await engine.isBlocked(event, { name: "my-platform", version: "1.0" }); -if (result.blocked) console.log(result.reason); -``` +✱ VS Code 1.109+ agent hooks use the Claude Code format. Empty cells mean this repo has no adapter, not that the host lacks the feature. ## CLI ```text ai-tools -ai-tools detect | sync | init | generate | install | status | clean +ai-tools detect | init | generate | install | status | clean | ui ``` +There is no top-level `sync`. Use `ai-tools mcp install` / `ai-tools mcp sync` from a config file. + | Engine | Job | |--------|-----| -| `hooks` | Lifecycle guardrails | -| `mcp` | MCP server config | +| `mcp` | MCP server config, scoped install, audit | | `skills` | Skills / prompts | | `agents` | Agent personas | | `rules` | Project rules | +| `hooks` | Lifecycle guardrails | | `plugins` | Portable bundles | | `sessions` | Cross-tool session read / handoff | +| `ui` | Terminal dashboard | + +Public install is **one package**: [`@itz4blitz/ai-tools`](https://www.npmjs.com/package/@itz4blitz/ai-tools). Workspaces under `packages/*` are how the engines are developed; they are not published separately. -Public install is **one package**: [`@itz4blitz/ai-tools`](https://www.npmjs.com/package/@itz4blitz/ai-tools). The `packages/*` workspaces in this repo are how the engines are developed; they are not separately published. +Canonical mode (`ai-tools init` / `generate` / `install`) still exists for a `.ai-tools/` store. Prefer per-engine `mcp.config.ts` (and friends) unless you already use canonical mode. ## Develop @@ -284,10 +216,9 @@ npm run check | Command | What it does | |---------|--------------| -| `npm run check` | lint + format + typecheck + coverage-gated tests + smoke | +| `npm run check` | production audit + lint + format + typecheck + coverage-gated tests + pack smoke | | `npm run build` | Turborepo build | | `npm test` | vitest | -| `npx vitest run packages/hooks` | one package | See [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/packages/cli/README.md b/packages/cli/README.md index 46ea917..3f3e8d0 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,157 +1,103 @@ -# ai-tools +# @itz4blitz/ai-tools [![CI](https://github.com/itz4blitz/ai-tools/actions/workflows/pipeline.yml/badge.svg)](https://github.com/itz4blitz/ai-tools/actions/workflows/pipeline.yml) [![npm](https://img.shields.io/npm/v/@itz4blitz/ai-tools.svg)](https://www.npmjs.com/package/@itz4blitz/ai-tools) ![Node.js >=22](https://img.shields.io/badge/node-%3E%3D22-339933?logo=node.js&logoColor=white) ![License MIT](https://img.shields.io/badge/license-MIT-16A34A) -**Write your AI-tool config once. `ai-tools` emits the native files each host actually reads.** +One CLI that writes the **native** config files each AI coding tool actually loads — MCP servers, skills, agents, rules, and hooks. -Hooks, MCP servers, skills, agents, and rules all have a different shape in Claude Code, Cursor, OpenCode, and Codex. This CLI is the translator — plus a plugin bundle layer so you can ship one capability pack onto the hosts installed on the machine. +Claude Code, Cursor, Codex, OpenCode, Grok, and ZCode do not share a format. Cursor wants `.cursor/mcp.json` with `"type": "http"`. Grok wants TOML `[mcp_servers]`. ZCode wants `mcp.servers` inside `~/.zcode/cli/config.json`. Skills live in `skills//SKILL.md` on the hosts that implement Agent Skills, and Cursor rules are flat `*.mdc` files. + +`ai-tools` is the translator. You describe the capability once. It emits the files the host reads. ```bash -npx @itz4blitz/ai-tools@latest detect +npm i -D @itz4blitz/ai-tools +npx ai-tools detect ``` -## Hosts +Requires Node 22+. -Primary fleet is Claude Code, Cursor, OpenCode, Codex, plus Grok and zcode next. The other adapters still ship. +## Do this, not that -| Host | Hooks | MCP | Agents | Skills | Rules | -|------|:-----:|:---:|:------:|:------:|:-----:| -| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | ✓ | ✓ | ✓ | ✓ | ✓ | -| [Cursor](https://cursor.com) | ✓ | ✓ | ✓ | ✓ | ✓ | -| [OpenCode](https://opencode.ai) | ✓ | ✓ | ✓ | ✓ | ✓ | -| [Codex](https://openai.com/codex/) | ✓ | ✓ | ✓ | ✓ | ✓ | -| Grok / grok build | ✓ | ✓ | ✓ | ✓ | ✓ | -| zcode | ✓ | ✓ | ✓ | ✓ | ✓ | -| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | ✓ | ✓ | ✓ | ✓ | ✓ | -| [Antigravity CLI](https://www.antigravity.google/product/antigravity-cli) | | | ✓ | ✓ | ✓ | -| [Kiro](https://kiro.dev) | ✓ | ✓ | ✓ | ✓ | ✓ | -| [Cline](https://cline.bot) | ✓ | ✓ | ✓ | ✓ | ✓ | -| [Amp](https://ampcode.com) | | ✓ | | ✓ | ✓ | -| [Factory Droid](https://factory.ai) | ✓ | ✓ | ✓ | ✓ | ✓ | -| [VS Code / Copilot](https://code.visualstudio.com) | ✱ | ✓ | ✓ | ✓ | ✓ | -| [Continue](https://continue.dev) | | | | ✓ | ✓ | -| [Roo Code](https://roocode.com) | | ✓ | ✓ | ✓ | ✓ | -| [Windsurf](https://windsurf.com) | | ✓ | | ✓ | ✓ | +**Install from a config file.** `ai-tools mcp install --layer=project` writes only the servers in `mcp.config.ts` that match the current repo. -✱ VS Code 1.109+ agent hooks use the Claude Code format. Grok and ZCode adapters cover hooks/MCP/agents/skills/rules. - -## 60-second path +**Do not copy one tool's live config onto every other tool.** That is how org MCP servers and interactive 1Password wrappers leak into user-global Cursor/Grok/ZCode configs. Unified `ai-tools sync` is disabled. `ai-tools mcp sync` is an alias of config-scoped install and **refuses** to run when the config selects nothing. ```bash -npm i -D @itz4blitz/ai-tools - -# What is installed on this machine? -npx ai-tools detect - -# One capability bundle → plan → native files -npx ai-tools plugins init -npx ai-tools plugins plan --tools=claude-code,cursor,opencode,codex -npx ai-tools plugins install --tools=claude-code,cursor,opencode,codex +npx ai-tools mcp audit # scan user-global MCP configs for those leaks ``` -Per-engine flow is the same everywhere: `init` → `detect` → `generate` → `install`. +## 60-second path ```bash -npx ai-tools hooks generate && npx ai-tools hooks install -npx ai-tools mcp generate && npx ai-tools mcp install -npx ai-tools skills generate && npx ai-tools skills install -npx ai-tools agents generate && npx ai-tools agents install -npx ai-tools rules generate && npx ai-tools rules install +npx ai-tools detect # what is installed on this machine +npx ai-tools mcp init # mcp.config.ts +# edit mcp.config.ts +npx ai-tools mcp install --layer=project # native files for detected hosts +npx ai-tools mcp audit ``` -Canonical mode keeps a single store under `.ai-tools/` and installs out from there: +Same shape for the other engines: ```bash -npx ai-tools init -npx ai-tools generate -npx ai-tools install -npx ai-tools status -``` - -```mermaid -graph TD - CFG["ai-*.config.ts / definePlugin()"] --> CLI["ai-tools CLI"] - - CLI --> Hooks - CLI --> MCP - CLI --> Agents - CLI --> Skills - CLI --> Rules - - subgraph fleet ["Primary"] - CC["Claude Code"] - Cursor["Cursor"] - OC["OpenCode"] - Codex["Codex"] - Grok["Grok / grok build"] - Zcode["zcode"] - end - - subgraph more ["Also"] - Gemini["Gemini CLI"] - Copilot["VS Code / Copilot"] - Cline["Cline"] - Kiro["Kiro"] - Antigravity["Antigravity CLI"] - Amp["Amp"] - Droid["Factory Droid"] - Continue["Continue"] - Roo["Roo Code"] - Windsurf["Windsurf"] - end - - Hooks --> fleet - MCP --> fleet - Agents --> fleet - Skills --> fleet - Rules --> fleet - Hooks --> more - MCP --> more - Agents --> more - Skills --> more - Rules --> more +npx ai-tools hooks init && npx ai-tools hooks install +npx ai-tools skills init && npx ai-tools skills install +npx ai-tools agents init && npx ai-tools agents install +npx ai-tools rules init && npx ai-tools rules install ``` -## Plugin bundle - -`definePlugin()` is the portable authoring surface. `plugins plan` tells you what each host can actually consume; `plugins install` writes only the supported pieces. +## MCP config ```ts -// ai-plugin.config.ts -import { definePlugin } from "@itz4blitz/ai-tools"; +// mcp.config.ts +import { defineConfig } from "@itz4blitz/ai-tools/mcp"; -export default definePlugin({ - id: "release-confidence", - name: "Release Confidence", - version: "0.1.0", - targets: { - include: ["claude-code", "cursor", "opencode", "codex"], - }, - mcpServers: [ +export default defineConfig({ + servers: [ { - id: "example-mcp", - name: "Example MCP", - transport: { type: "stdio", command: "npx", args: ["-y", "example-mcp"] }, + id: "github", + name: "GitHub", + transport: { type: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] }, + layer: "user", }, - ], - skills: [ { - id: "ship-check", - name: "Ship Check", - content: "Run the failing test first, then the smallest fix that makes it pass.", + id: "internal-wiki", + name: "Internal wiki", + transport: { type: "stdio", command: "wiki-mcp" }, + layer: "project", + whenPathContains: ["internal-wiki", "docs-site"], }, ], }); ``` -Hosts are not identical. Cursor and Codex can take a broad bundle. OpenCode has MCP plus a native plugin system. Claude Code is project-level hooks, agents, commands, and MCP. +| Field | Effect | +|-------|--------| +| `layer: "user"` | Eligible for user-global install (`--layer=user`) | +| `layer: "project"` (default) | Eligible for repo install (`--layer=project`) | +| `whenPathContains` | Server is skipped unless `process.cwd()` contains one of the fragments | + +Empty config → install/sync/generate **exit 1**. They will not write `{}` over a working host file. + +### What gets written -## Config examples +| Host | Project file | User file | +|------|----------------|-----------| +| Cursor | `.cursor/mcp.json` (`mcpServers`, HTTP/SSE include `type`) | `~/.cursor/mcp.json` | +| Claude Code | `.mcp.json` | `~/.claude.json` | +| Grok | `.grok/config.toml` `[mcp_servers]` | `~/.grok/config.toml` | +| Codex | `.codex/config.toml` `[mcp_servers]` | `~/.codex/config.toml` | +| OpenCode | `opencode.json` `mcp` | `~/.config/opencode/opencode.json` | +| ZCode | `.zcode/config.json` `mcp.servers` | `~/.zcode/cli/config.json` | -### Hooks +ZCode's leftover `.zcode/mcp.json` is **not** what the CLI reads. This package does not write it. + +## Skills, agents, rules, hooks + +Where the host implements Agent Skills, generate writes `skills//SKILL.md` (Grok, ZCode, Cursor, Claude Code, Codex, OpenCode). Hosts that only have prompt files keep those (Amp, Cline, Continue, …). + +Cursor rules are flat `.cursor/rules/.mdc`. OpenCode agents are `.opencode/agent/.md` (singular). Grok/ZCode hooks write JSON the host loads and a runner that actually executes `HookEngine` — not a stub `process.exit(0)`. ```ts // ai-hooks.config.ts @@ -172,106 +118,92 @@ export default defineConfig({ .priority(10) .build(), ], - settings: { hookTimeout: 5000, failMode: "open", logLevel: "warn" }, }); ``` -### MCP +Built-in guardrails: `block-dangerous-commands`, `scan-secrets`, `protect-sensitive-files`, `audit-shell`. + +## Plugin bundle + +`definePlugin()` is one file that can carry MCP + skills + agents + rules + hooks. `plugins plan` shows what each host can consume. `plugins install` writes only those pieces, and MCP servers still go through `layer` / `whenPathContains`. ```ts -// mcp.config.ts -import { defineConfig } from "@itz4blitz/ai-tools/mcp"; +// ai-plugin.config.ts +import { definePlugin } from "@itz4blitz/ai-tools"; -export default defineConfig({ - servers: [ - { - id: "filesystem", - name: "filesystem", - transport: { - type: "stdio", - command: "npx", - args: ["-y", "@modelcontextprotocol/server-filesystem", "./src"], - }, - layer: "project", - }, +export default definePlugin({ + id: "release-confidence", + name: "Release Confidence", + version: "0.1.0", + targets: { include: ["claude-code", "cursor", "opencode", "codex"] }, + mcpServers: [ { - id: "palamhealth", - name: "PalamHealth", - transport: { type: "stdio", command: "palamhealth-mcp" }, - layer: "project", - whenPathContains: ["PalamHealth"], + id: "example-mcp", + name: "Example MCP", + transport: { type: "stdio", command: "npx", args: ["-y", "example-mcp"] }, }, ], -}); -``` - -`ai-tools mcp install --layer=project` writes only matching servers. `ai-tools mcp sync` never copies imported servers between tools. `ai-tools mcp audit` reports PalamHealth leaks in user-global configs. - -### Rules - -```ts -// ai-rules.config.ts -import { defineRulesConfig } from "@itz4blitz/ai-tools/rules"; - -export default defineRulesConfig({ - rules: [ + skills: [ { - id: "typescript-strict", - name: "typescript-strict", - content: "Always use strict TypeScript. No `any` types.", - scope: { type: "always" }, - priority: 1, + id: "ship-check", + name: "Ship Check", + content: "Run the failing test first, then the smallest fix that makes it pass.", }, ], }); ``` -## Hooks runtime - -Express-style middleware over a 15-event model. Before-events can block; after-events are observe-only. - -| Category | Before (blockable) | After | -|----------|--------------------|-------| -| Session | `session:start` | `session:end` | -| Prompt | `prompt:submit` | `prompt:response` | -| Tool | `tool:before` | `tool:after` | -| File | `file:read`, `file:write`, `file:edit`, `file:delete` | | -| Shell | `shell:before` | `shell:after` | -| MCP | `mcp:before` | `mcp:after` | -| System | | `notification` | - -Built-in guardrails: `block-dangerous-commands`, `scan-secrets`, `protect-sensitive-files`, `audit-shell`. +```bash +npx ai-tools plugins plan --tools=claude-code,cursor,opencode,codex +npx ai-tools plugins install --tools=claude-code,cursor,opencode,codex +``` -```ts -import { HookEngine, builtinHooks } from "@itz4blitz/ai-tools/hooks"; +## Hosts -const engine = new HookEngine({ - hooks: builtinHooks, - settings: { failMode: "closed" }, -}); +| Host | Hooks | MCP | Agents | Skills | Rules | +|------|:-----:|:---:|:------:|:------:|:-----:| +| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | ✓ | ✓ | ✓ | ✓ | ✓ | +| [Cursor](https://cursor.com) | ✓ | ✓ | ✓ | ✓ | ✓ | +| [OpenCode](https://opencode.ai) | ✓ | ✓ | ✓ | ✓ | ✓ | +| [Codex](https://openai.com/codex/) | ✓ | ✓ | ✓ | ✓ | ✓ | +| Grok | ✓ | ✓ | ✓ | ✓ | ✓ | +| ZCode | ✓ | ✓ | ✓ | ✓ | ✓ | +| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | ✓ | ✓ | ✓ | ✓ | ✓ | +| [Antigravity CLI](https://www.antigravity.google/product/antigravity-cli) | | | ✓ | ✓ | ✓ | +| [Kiro](https://kiro.dev) | ✓ | ✓ | ✓ | ✓ | ✓ | +| [Cline](https://cline.bot) | ✓ | ✓ | ✓ | ✓ | ✓ | +| [Amp](https://ampcode.com) | | ✓ | | ✓ | ✓ | +| [Factory Droid](https://factory.ai) | ✓ | ✓ | ✓ | ✓ | ✓ | +| [VS Code / Copilot](https://code.visualstudio.com) | ✱ | ✓ | ✓ | ✓ | ✓ | +| [Continue](https://continue.dev) | | | | ✓ | ✓ | +| [Roo Code](https://roocode.com) | | ✓ | ✓ | ✓ | ✓ | +| [Windsurf](https://windsurf.com) | | ✓ | | ✓ | ✓ | -const result = await engine.isBlocked(event, { name: "my-platform", version: "1.0" }); -if (result.blocked) console.log(result.reason); -``` +✱ VS Code 1.109+ agent hooks use the Claude Code format. Empty cells mean this repo has no adapter, not that the host lacks the feature. ## CLI ```text ai-tools -ai-tools detect | sync | init | generate | install | status | clean +ai-tools detect | init | generate | install | status | clean | ui ``` +There is no top-level `sync`. Use `ai-tools mcp install` / `ai-tools mcp sync` from a config file. + | Engine | Job | |--------|-----| -| `hooks` | Lifecycle guardrails | -| `mcp` | MCP server config | +| `mcp` | MCP server config, scoped install, audit | | `skills` | Skills / prompts | | `agents` | Agent personas | | `rules` | Project rules | +| `hooks` | Lifecycle guardrails | | `plugins` | Portable bundles | | `sessions` | Cross-tool session read / handoff | +| `ui` | Terminal dashboard | + +Public install is **one package**: [`@itz4blitz/ai-tools`](https://www.npmjs.com/package/@itz4blitz/ai-tools). Workspaces under `packages/*` are how the engines are developed; they are not published separately. -Public install is **one package**: [`@itz4blitz/ai-tools`](https://www.npmjs.com/package/@itz4blitz/ai-tools). The `packages/*` workspaces in this repo are how the engines are developed; they are not separately published. +Canonical mode (`ai-tools init` / `generate` / `install`) still exists for a `.ai-tools/` store. Prefer per-engine `mcp.config.ts` (and friends) unless you already use canonical mode. ## Develop @@ -284,10 +216,9 @@ npm run check | Command | What it does | |---------|--------------| -| `npm run check` | lint + format + typecheck + coverage-gated tests + smoke | +| `npm run check` | production audit + lint + format + typecheck + coverage-gated tests + pack smoke | | `npm run build` | Turborepo build | | `npm test` | vitest | -| `npx vitest run packages/hooks` | one package | See [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/packages/cli/src/cli/index.test.ts b/packages/cli/src/cli/index.test.ts index fa7f1fd..3528120 100644 --- a/packages/cli/src/cli/index.test.ts +++ b/packages/cli/src/cli/index.test.ts @@ -104,6 +104,7 @@ describe("run() - help output", () => { expect(crossCutting).not.toMatch(/\bsync\b/); expect(output).toContain("ai-tools mcp install --layer=project"); expect(output).toContain("ai-tools mcp sync --layer=project"); + expect(output).not.toMatch(/PalamHealth/); }); it("does not claim canonical install uses a CanonicalStore", async () => { diff --git a/packages/cli/src/cli/index.ts b/packages/cli/src/cli/index.ts index 401d9ce..37a019c 100644 --- a/packages/cli/src/cli/index.ts +++ b/packages/cli/src/cli/index.ts @@ -42,7 +42,7 @@ EXAMPLES: ${cliName} mcp detect # Detect MCP-capable tools ${cliName} mcp install --layer=project # Install from mcp.config.ts ${cliName} mcp sync --layer=project # Config-scoped MCP install (never copies imports) - ${cliName} mcp audit # Find PalamHealth user-global leaks + ${cliName} mcp audit # Scan user-global MCP configs for leaks ${cliName} detect # Detect across all engines ${cliName} plugins plan --tools=cursor,codex,opencode ${cliName} hooks init # Initialize hooks config diff --git a/packages/cli/src/release/smoke-published.test.ts b/packages/cli/src/release/smoke-published.test.ts new file mode 100644 index 0000000..94f7cd1 --- /dev/null +++ b/packages/cli/src/release/smoke-published.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it, vi } from "vitest"; +import { + isPublishedHelpHealthy, + PUBLISHED_HELP_MARKER, + smokePublished, +} from "../../../../scripts/smoke-published.mjs"; +import { run as runCli } from "../cli/index.js"; + +describe("published help marker", () => { + it("matches the live CLI help string", async () => { + const lines: string[] = []; + const original = console.log; + console.log = (...args: unknown[]) => { + lines.push(args.map(String).join(" ")); + }; + try { + await runCli(["help"]); + } finally { + console.log = original; + } + const help = lines.join("\n"); + expect(help).toMatch(PUBLISHED_HELP_MARKER); + expect(isPublishedHelpHealthy(0, help)).toBe(true); + expect(isPublishedHelpHealthy(1, help)).toBe(false); + expect(isPublishedHelpHealthy(0, "npx: command not found")).toBe(false); + }); +}); + +describe("smokePublished", () => { + it("retries, prints the failing output, then succeeds", async () => { + const logs: string[] = []; + const runHelp = vi + .fn() + .mockReturnValueOnce({ status: 1, stdout: "", stderr: "E404 Not Found" }) + .mockReturnValueOnce({ + status: 0, + stdout: "ai-tools - Unified CLI for ai-tools engines\n", + stderr: "", + }); + + const code = await smokePublished({ + maxAttempts: 3, + waitMs: 1, + runHelp, + sleep: () => undefined, + log: (line) => logs.push(String(line)), + error: (line) => logs.push(String(line)), + }); + + expect(code).toBe(0); + expect(runHelp).toHaveBeenCalledTimes(2); + expect(logs.join("\n")).toContain("E404 Not Found"); + expect(logs.join("\n")).toContain("ai-tools published smoke passed"); + }); + + it("exits 1 after logging every failed attempt", async () => { + const logs: string[] = []; + const code = await smokePublished({ + maxAttempts: 2, + waitMs: 1, + runHelp: () => ({ status: 1, stdout: "", stderr: "network timeout" }), + sleep: () => undefined, + log: (line) => logs.push(String(line)), + error: (line) => logs.push(String(line)), + }); + + expect(code).toBe(1); + expect(logs.filter((line) => line.includes("network timeout"))).toHaveLength(2); + expect(logs.at(-1)).toBe("ai-tools published smoke failed after retries"); + }); +}); diff --git a/packages/mcp/src/cli/index.ts b/packages/mcp/src/cli/index.ts index 035b468..f54bd43 100644 --- a/packages/mcp/src/cli/index.ts +++ b/packages/mcp/src/cli/index.ts @@ -22,7 +22,7 @@ COMMANDS: import Import MCP servers from an existing tool's config sync Install servers from mcp.config.ts onto detected tools (never copies imports) export Export MCP server definitions as JSON - audit Find PalamHealth / interactive-1Password leaks in user-global configs + audit Find org-specific / interactive-1Password leaks in user-global configs help Show this help message OPTIONS: @@ -37,7 +37,7 @@ EXAMPLES: ai-mcp install --tools=claude-code # Install MCP servers for Claude Code only ai-mcp install --layer=project # Project-scoped install from mcp.config.ts ai-mcp sync --layer=project # Config-scoped install (never copies imports) - ai-mcp audit # Find PalamHealth leaks in user-global configs + ai-mcp audit # Scan user-global MCP configs for leaks ai-mcp import --tools=claude-code # Import servers from Claude Code config ai-mcp export # Export all MCP servers as JSON `; diff --git a/scripts/smoke-published.mjs b/scripts/smoke-published.mjs new file mode 100644 index 0000000..eba9e5c --- /dev/null +++ b/scripts/smoke-published.mjs @@ -0,0 +1,55 @@ +#!/usr/bin/env node + +import { spawnSync } from "node:child_process"; + +/** Stable substring of `ai-tools help` that post-publish CI asserts. */ +export const PUBLISHED_HELP_MARKER = /ai-tools\s+-\s+Unified CLI/; + +export function isPublishedHelpHealthy(status, output) { + return status === 0 && PUBLISHED_HELP_MARKER.test(output); +} + +export function runHelp(pkg = "@itz4blitz/ai-tools@latest") { + const npx = process.platform === "win32" ? "npx.cmd" : "npx"; + return spawnSync(npx, ["--yes", "--package", pkg, "ai-tools", "help"], { + encoding: "utf-8", + shell: process.platform === "win32", + env: { ...process.env, npm_config_yes: "true" }, + }); +} + +export async function smokePublished(options = {}) { + const pkg = options.pkg ?? process.env.AI_TOOLS_SMOKE_PACKAGE ?? "@itz4blitz/ai-tools@latest"; + const maxAttempts = Number(options.maxAttempts ?? process.env.AI_TOOLS_SMOKE_ATTEMPTS ?? 12); + const waitMs = Number(options.waitMs ?? process.env.AI_TOOLS_SMOKE_WAIT_MS ?? 15000); + const run = options.runHelp ?? runHelp; + const sleep = + options.sleep ?? + ((ms) => { + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); + }); + const log = options.log ?? console.log; + const error = options.error ?? console.error; + + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + const result = run(pkg); + const output = `${result.stdout ?? ""}${result.stderr ?? ""}`; + if (isPublishedHelpHealthy(result.status, output)) { + log("ai-tools published smoke passed"); + return 0; + } + log(`attempt ${attempt}/${maxAttempts} failed (status=${result.status})`); + log(output.trim() || "(no stdout/stderr)"); + if (attempt < maxAttempts) sleep(waitMs); + } + + error("ai-tools published smoke failed after retries"); + return 1; +} + +const invokedDirectly = process.argv[1] + ?.replaceAll("\\", "/") + .endsWith("scripts/smoke-published.mjs"); +if (invokedDirectly) { + process.exit(await smokePublished()); +}