diff --git a/.tegami/2026-07-01-dev-watch.md b/.tegami/2026-07-01-dev-watch.md new file mode 100644 index 0000000..8f0027c --- /dev/null +++ b/.tegami/2026-07-01-dev-watch.md @@ -0,0 +1,10 @@ +--- +packages: + "@jalco/ap-sdk": minor +--- + +## Add `ap-sdk dev` + +Watch mode for plugin authors: `ap-sdk dev` rebuilds on every change to the +plugin and its referenced files, and `--install` drops the result straight +into your local harness dirs. Errors keep the watcher alive. diff --git a/.tegami/2026-07-01-init-command.md b/.tegami/2026-07-01-init-command.md new file mode 100644 index 0000000..6f5c9b0 --- /dev/null +++ b/.tegami/2026-07-01-init-command.md @@ -0,0 +1,10 @@ +--- +packages: + "@jalco/ap-sdk": minor +--- + +## Add `ap-sdk init` + +Scaffold a new plugin project with one command. `ap-sdk init my-plugin` writes +a working `plugin.ts` (a skill, a command, and instructions) that passes +`ap-sdk check` as-is, plus a `.gitignore` entry for `.aps-out/`. diff --git a/.tegami/2026-07-01-npm-source.md b/.tegami/2026-07-01-npm-source.md new file mode 100644 index 0000000..ce54304 --- /dev/null +++ b/.tegami/2026-07-01-npm-source.md @@ -0,0 +1,12 @@ +--- +packages: + "@jalco/ap-sdk": minor +--- + +## Install plugins from npm + +`ap-sdk install npm:` (and `check`/`build` with the same spec) +fetches a published package from the npm registry, verifies it's a +compatible plugin, and installs it — with `npm:@` for +pinning. Packages can point at a non-root plugin file via an +`ap-sdk.plugin` field in package.json. diff --git a/.tegami/2026-07-01-package-readme.md b/.tegami/2026-07-01-package-readme.md new file mode 100644 index 0000000..52ea489 --- /dev/null +++ b/.tegami/2026-07-01-package-readme.md @@ -0,0 +1,10 @@ +--- +packages: + "@jalco/ap-sdk": patch +--- + +### Add a package README + +The npm page for `@jalco/ap-sdk` now has a readme covering the full feature +surface (skills, commands, subagents, hooks, MCP, shared tools) and the +`ap-sdk port` migration path. diff --git a/.tegami/2026-07-01-uninstall.md b/.tegami/2026-07-01-uninstall.md new file mode 100644 index 0000000..cbd6023 --- /dev/null +++ b/.tegami/2026-07-01-uninstall.md @@ -0,0 +1,11 @@ +--- +packages: + "@jalco/ap-sdk": minor +--- + +## Add `ap-sdk uninstall` + +`install` now records what it wrote to an install manifest +(`.ap-sdk/install-manifest.json`), and `ap-sdk uninstall ` cleanly +reverses it — deleting the plugin's files and removing only its entries from +merged configs (instruction blocks, MCP servers, hooks). diff --git a/README.md b/README.md index b017be9..5cb91ee 100644 --- a/README.md +++ b/README.md @@ -39,13 +39,40 @@ ap-sdk build # → .aps-out/{claude,codex,gemini,copilot,cursor,windsurf,pi ap-sdk install # → drops artifacts into your local harness dirs ``` +## What you can define + +| Field | What it becomes | +| --- | --- | +| [`instructions`](https://ap-sdk.dev/docs) | Always-on system or project guidance for every supported harness. | +| [`skills`](https://ap-sdk.dev/docs) | Reusable skill documents with descriptions and instructions. | +| [`commands`](https://ap-sdk.dev/docs) | Slash commands or prompt commands in harnesses that support them. | +| [`subagents`](https://ap-sdk.dev/docs) | Named specialist agents with their own prompts, tools, and model hints. | +| [`hooks`](https://ap-sdk.dev/docs) | Lifecycle hooks and scripts translated to each harness' event model. | +| [`mcpServers`](https://ap-sdk.dev/docs) | MCP server config emitted in native JSON/TOML/YAML formats. | +| [`tools`](https://ap-sdk.dev/docs) | Shared TypeScript tool implementations with per-harness glue. | +| [`files`](https://ap-sdk.dev/docs) | Companion files copied next to the generated native artifacts. | + +See the [support matrix](https://ap-sdk.dev/docs/harnesses) for what each harness supports. + +## Already have a plugin? + +Point `ap-sdk port` at an existing Claude Code, Cursor, Codex, Gemini, Copilot, Windsurf, Pi, or OpenCode layout and it generates a portable `plugin.ts` that loads your existing files instead of inlining them. + +```bash +npx ap-sdk port ./my-plugin +npx ap-sdk check ./plugin.ts +npx ap-sdk install owner/repo +``` + +Read the [porting guide](https://ap-sdk.dev/docs/porting), or install straight from GitHub with `ap-sdk install owner/repo`. + ## Install ```bash pnpm add -D @jalco/ap-sdk ``` -[Docs](https://github.com/jal-co/agent-plugin-sdk) · [Examples](./packages/agent-plugin-sdk/examples) +[Docs](https://ap-sdk.dev/docs) · [Examples](https://github.com/jal-co/agent-plugin-sdk/tree/main/packages/agent-plugin-sdk/examples) ## License diff --git a/apps/docs/content/docs/api/harness.mdx b/apps/docs/content/docs/api/harness.mdx new file mode 100644 index 0000000..174d74c --- /dev/null +++ b/apps/docs/content/docs/api/harness.mdx @@ -0,0 +1,44 @@ +--- +title: Harness entrypoint +description: Harness authoring toolkit for custom targets. +--- + +## defineHarness + +Identity helper for a custom target harness: support map, emit translator, install paths, and native config behavior. + +```ts +export function defineHarness(harness: Harness): Harness; +``` + +## registerHarness + +Registers a custom harness id so build, install, and CLI target validation can use it. + +```ts +export function registerHarness(harness: Harness): void; +``` + +## Harness + +The contract every built-in and custom harness implements: id, display name, support map, emit function, and install path helpers. + +```ts +export interface Harness { id: HarnessId; displayName: string; supports: FeatureSupport; ... } +``` + +## EmitContext + +Context passed to emitters, including helper methods and target metadata. + +```ts +export interface EmitContext { harness: Harness; ... } +``` + +## InstallScope + +Where install writes artifacts: project directories or home-directory global config. + +```ts +export type InstallScope = 'project' | 'global'; +``` diff --git a/apps/docs/content/docs/api/index.mdx b/apps/docs/content/docs/api/index.mdx new file mode 100644 index 0000000..2b0b447 --- /dev/null +++ b/apps/docs/content/docs/api/index.mdx @@ -0,0 +1,13 @@ +--- +title: API reference +description: Generated reference for the public ap-sdk entrypoints. +--- + +This section is generated from source-aligned API metadata. Regenerate it with `pnpm --filter @jal-co/docs generate:api`. + + + + + + + diff --git a/apps/docs/content/docs/api/main.mdx b/apps/docs/content/docs/api/main.mdx new file mode 100644 index 0000000..18ff518 --- /dev/null +++ b/apps/docs/content/docs/api/main.mdx @@ -0,0 +1,84 @@ +--- +title: Main entrypoint +description: Public helpers and types exported by @jalco/ap-sdk. +--- + +## definePlugin + +Identity helper for a portable plugin definition. Author once, then build or install native artifacts for each harness. + +```ts +export function definePlugin(plugin: Plugin): Plugin; +``` + +## defineSkill + +Defines a portable agent skill with a trigger description, instructions, optional resources, and harness metadata. + +```ts +export function defineSkill(skill: Skill): Skill; +``` + +## defineCommand + +Defines a portable slash command or prompt command with argument templating and per-harness overrides. + +```ts +export function defineCommand(command: Command): Command; +``` + +## defineSubagent + +Defines a specialist agent prompt with optional tools and target-specific model settings. + +```ts +export function defineSubagent(subagent: Subagent): Subagent; +``` + +## defineHook + +Defines a lifecycle hook using ap-sdk's portable event names and optional native overrides. + +```ts +export function defineHook(hook: Hook): Hook; +``` + +## Plugin + +The root declaration: id, description, instructions, skills, commands, MCP servers, subagents, hooks, files, tools, and marketplace metadata. + +```ts +export interface Plugin { id: string; description: string; ... } +``` + +## Skill + +Reusable instructions plus metadata. Descriptions are capped to the tightest harness limit and drive skill routing. + +```ts +export interface Skill { name: string; description: string; instructions: string; ... } +``` + +## Hook + +Portable hook event, matcher, command, timeout, async flag, and per-harness event or matcher overrides. + +```ts +export interface Hook { event: HookEvent; command: string | HookCommand; ... } +``` + +## build + +Validates a plugin and returns in-memory output files and warnings for each requested harness. + +```ts +export function build(plugin: Plugin, options?: BuildOptions): HarnessBuild[]; +``` + +## installSkills + +Installs emitted skills, commands, MCP config, hooks, instructions, subagents, and files into live harness directories. + +```ts +export function installSkills(plugin: Plugin, options?: InstallOptions): InstalledItem[]; +``` diff --git a/apps/docs/content/docs/api/meta.json b/apps/docs/content/docs/api/meta.json new file mode 100644 index 0000000..a418fd9 --- /dev/null +++ b/apps/docs/content/docs/api/meta.json @@ -0,0 +1,4 @@ +{ + "title": "API", + "pages": ["index", "main", "runtime", "harness"] +} diff --git a/apps/docs/content/docs/api/runtime.mdx b/apps/docs/content/docs/api/runtime.mdx new file mode 100644 index 0000000..b58a7cc --- /dev/null +++ b/apps/docs/content/docs/api/runtime.mdx @@ -0,0 +1,36 @@ +--- +title: Runtime entrypoint +description: Runtime helpers for portable tools. +--- + +## defineTool + +Defines one executable tool with metadata, JSON schema parameters, and a single handler shared across harness adapters. + +```ts +export function defineTool(tool: Tool): Tool; +``` + +## listTools + +Loads a tools module and returns the declared tool metadata for local inspection or generated adapters. + +```ts +export async function listTools(modulePath: string): Promise; +``` + +## callTool + +Invokes a named tool locally with JSON arguments, matching the CLI's `ap-sdk tools --call` loop. + +```ts +export async function callTool(modulePath: string, name: string, args: unknown): Promise; +``` + +## contentToText + +Converts structured tool content blocks into plain text for terminal output. + +```ts +export function contentToText(content: ToolContent[]): string; +``` diff --git a/apps/docs/content/docs/commands.mdx b/apps/docs/content/docs/commands.mdx new file mode 100644 index 0000000..ef3dc5f --- /dev/null +++ b/apps/docs/content/docs/commands.mdx @@ -0,0 +1,45 @@ +--- +title: Commands +description: Define portable slash commands and prompt commands with argument templates. +--- + +Commands compile to each harness' native command or prompt file. Use them for repeatable workflows where the user explicitly invokes an action. + +```ts title="plugin.ts" +import { defineCommand, definePlugin } from "@jalco/ap-sdk"; + +export default definePlugin({ + id: "issue-tools", + description: "Issue triage commands.", + commands: [ + defineCommand({ + name: "fix-issue", + description: "Fix a GitHub issue end to end.", + argumentHint: "", + allowedTools: ["Bash", "Read", "Edit"], + body: "Fix issue #$1. Use $ARGUMENTS as the raw user request.", + harness: { claude: { model: "sonnet" }, opencode: { model: "gpt-5" } }, + }), + ], +}); +``` + +## Argument templates + +| Template | Meaning | Portability | +| --- | --- | --- | +| `$ARGUMENTS` | The complete argument string. | Portable everywhere. | +| `$1`, `$2` | 1-based positional args in ap-sdk source. | Claude is rewritten to its 0-based native form. | +| `` !`shell` `` | Native shell interpolation. | Passed through for Claude and OpenCode. | +| `@file` | Native file reference. | Passed through for Claude and OpenCode. | + +## Fields + +- `name`, `description`, `argumentHint`, and `body` define the command. +- `allowedTools` is honored by Claude Code. +- `frontmatter` is honored by YAML-frontmatter harnesses; Gemini TOML and Cursor plain markdown ignore it. +- `harness.claude.model`, `harness.opencode.model`, and `harness.copilot.model` / `agent` set native model hints. + +## Portability notes + +If a harness cannot represent command metadata, the command body still emits and the build reports a warning for the unsupported option. Next: [subagents](/docs/subagents) and the [support matrix](/docs/harnesses). diff --git a/apps/docs/content/docs/hooks.mdx b/apps/docs/content/docs/hooks.mdx new file mode 100644 index 0000000..36c2aaa --- /dev/null +++ b/apps/docs/content/docs/hooks.mdx @@ -0,0 +1,53 @@ +--- +title: Hooks +description: Translate lifecycle hooks and scripts across harness event models. +--- + +Hooks run commands around tool calls, prompts, sessions, and agent stops. ap-sdk uses a portable event vocabulary and translates native event spellings where a harness supports hooks. + +```ts title="plugin.ts" +import { defineHook, definePlugin } from "@jalco/ap-sdk"; + +export default definePlugin({ + id: "plan-review", + description: "Review plans before execution.", + hooks: [ + defineHook({ + event: "pre-tool-use", + matcher: "ExitPlanMode", + command: { unix: "node hooks/plan-review.mjs", powershell: "node hooks/plan-review.mjs" }, + timeout: 120, + async: true, + harness: { codex: { event: "stop", matcher: undefined } }, + }), + ], +}); +``` + +## Portable events + +| ap-sdk event | Claude | Copilot | Gemini | +| --- | --- | --- | --- | +| `pre-tool-use` | `PreToolUse` | `preToolUse` | `BeforeTool` | +| `post-tool-use` | `PostToolUse` | `postToolUse` | `AfterTool` | +| `stop` | `Stop` | `stop` | `Stop` | +| `user-prompt-submit` | `UserPromptSubmit` | `userPromptSubmit` | `UserPromptSubmit` | +| `session-start` | `SessionStart` | `sessionStart` | `SessionStart` | +| `notification` | `Notification` | `notification` | `Notification` | +| `permission-request` | `PermissionRequest` | `permissionRequest` | `PermissionRequest` | +| `subagent-stop` | `SubagentStop` | `subagentStop` | `SubagentStop` | +| `pre-compact` | `PreCompact` | `preCompact` | `PreCompact` | +| `session-end` | `SessionEnd` | `sessionEnd` | `SessionEnd` | + +## Fields + +- `event`, `matcher`, `command`, and `timeout` define the hook. +- `async` is native in Claude and dropped with a warning elsewhere. +- Object commands can provide `unix` and `powershell`; PowerShell is used by Copilot. +- `harness..event` and `matcher` override native mapping. Use `matcher: undefined` to explicitly clear a matcher. + + +Pi and OpenCode hooks are code-only today; builds note the limitation instead of emitting a misleading artifact. + + +Next: [MCP servers](/docs/mcp) and the [support matrix](/docs/harnesses). diff --git a/apps/docs/content/docs/index.mdx b/apps/docs/content/docs/index.mdx index 1d9f366..ca2c070 100644 --- a/apps/docs/content/docs/index.mdx +++ b/apps/docs/content/docs/index.mdx @@ -12,6 +12,9 @@ compiles it to the **native installable artifacts** each harness expects. No runtime, no wrapper — the output is exactly the files those harnesses load on their own. +Already have a plugin for one harness? Start with [porting](/docs/porting) +instead and let `ap-sdk port` generate the portable definition from your files. + ```ts title="plugin.ts" import { definePlugin, defineSkill } from "@jalco/ap-sdk"; @@ -62,6 +65,10 @@ Harnesses share a common feature set, but each also diverges. The SDK borrows + + + + diff --git a/apps/docs/content/docs/installing-plugins.mdx b/apps/docs/content/docs/installing-plugins.mdx index 3f1f405..0180451 100644 --- a/apps/docs/content/docs/installing-plugins.mdx +++ b/apps/docs/content/docs/installing-plugins.mdx @@ -12,6 +12,8 @@ the skills, commands, and tools immediately. npx ap-sdk install ``` +Looking for plugins? Browse the [plugin directory](/docs/plugins). + Install reuses the exact same output as `build`, so installed files are byte-for-byte identical — it just relocates each feature into the harness's live config dir instead of an output folder. @@ -105,6 +107,51 @@ Use `--path ` when the plugin lives in a subdirectory, and set `@jalco/ap-sdk` import is resolved to the SDK running the install, so simple plugins need no dependency install of their own. +## From npm + +Use the `npm:` source form to fetch a published package, run the same +compatibility check, and install only if it default-exports a valid ap-sdk +plugin: + +```bash compact +npx ap-sdk install npm:@acme/git-helper +npx ap-sdk install npm:@acme/git-helper@1.2.3 -t claude +npx ap-sdk check npm:@acme/git-helper@next +``` + +Version pinning uses the suffix after the package name (`npm:name@1.2.3` or a +dist-tag like `npm:name@next`). Scoped names keep their scope: +`npm:@scope/name@1.2.3`. + +Packages can point at a non-root plugin file with package metadata: + +```json title="package.json" +{ + "ap-sdk": { "plugin": "./dist/plugin.js" } +} +``` + +If that field is absent, the CLI looks for the usual `plugin.ts`, `plugin.js`, +or `ap-sdk.config.ts`; use `--path ` for packages that contain the plugin +inside a subdirectory. + +## Uninstalling + +Every non-dry-run install records exactly what was written in +`.ap-sdk/install-manifest.json` for project installs, or +`~/.ap-sdk/install-manifest.json` for global installs. Remove a recorded plugin +with: + +```bash compact +npx ap-sdk uninstall git-helper +npx ap-sdk uninstall git-helper --dry-run +npx ap-sdk uninstall git-helper --global +``` + +Uninstall deletes only recorded files and removes only the plugin's entries from +shared configs (instruction blocks, MCP servers, hooks). Installs made before +this manifest existed have no record, so they cannot be auto-removed. + ## Programmatic install Everything the CLI does is available as a function, so you can install from your diff --git a/apps/docs/content/docs/instructions-and-files.mdx b/apps/docs/content/docs/instructions-and-files.mdx new file mode 100644 index 0000000..ebe1194 --- /dev/null +++ b/apps/docs/content/docs/instructions-and-files.mdx @@ -0,0 +1,37 @@ +--- +title: Instructions and files +description: Ship always-on instructions and companion files alongside generated artifacts. +--- + +A plugin can include global instructions plus arbitrary files. Instructions become native context files (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`) and installs merge them as id-keyed blocks; files are copied verbatim into build trees. + +```ts title="plugin.ts" +import { definePlugin, defineSkill, readDir, readTextFrom } from "@jalco/ap-sdk"; + +const read = readTextFrom(import.meta.url); + +export default definePlugin({ + id: "repo-playbook", + description: "Repository-specific playbook.", + instructions: read("./AGENTS.md"), + skills: [ + defineSkill({ + name: "release-check", + description: "Use before preparing a release.", + instructions: read("./skills/release/SKILL.md"), + resources: [{ path: "checklist.md", content: read("./skills/release/checklist.md") }], + }), + ], + files: readDir("./hooks", import.meta.url, "hooks"), +}); +``` + +## Fields and helpers + +- `instructions` is always-on guidance. Install uses an id-keyed markdown block so re-installs update the SDK-managed section without clobbering foreign text. +- `files` contains `PluginFile` entries emitted into every build tree; `executable` preserves script bits. +- `SkillResource` files live inside a skill directory. `PluginFile` files live at the plugin root for scripts, templates, and shared assets. +- `readText`, `readTextFrom`, and `readDir` keep content on disk and avoid giant inline strings. +- Plugin-root variables such as `${CLAUDE_PLUGIN_ROOT}/hooks/notify.sh` let native configs reference emitted companion files. + +Next: [skills](/docs/skills), [porting](/docs/porting), and the [support matrix](/docs/harnesses). diff --git a/apps/docs/content/docs/mcp.mdx b/apps/docs/content/docs/mcp.mdx new file mode 100644 index 0000000..a2a6352 --- /dev/null +++ b/apps/docs/content/docs/mcp.mdx @@ -0,0 +1,40 @@ +--- +title: MCP servers +description: Declare stdio and HTTP MCP servers once and emit native config. +--- + +MCP servers are configuration, not code. ap-sdk emits native MCP config for harnesses that support it and warns for those that do not. + +```ts title="plugin.ts" +import { definePlugin } from "@jalco/ap-sdk"; + +export default definePlugin({ + id: "repo-context", + description: "Repository context MCP servers.", + mcpServers: { + github: { + command: "npx", + args: ["-y", "@modelcontextprotocol/server-github"], + env: { GITHUB_TOKEN: "${GITHUB_TOKEN}" }, + }, + docs: { + transport: "http", + url: "https://docs.example.com/mcp", + headers: { Authorization: "Bearer ${DOCS_TOKEN}" }, + }, + }, +}); +``` + +## Fields + +- Stdio servers use `command`, `args`, `env`, and `cwd`. +- HTTP servers use `transport: "http"`, `url`, and `headers`. +- Claude and Codex receive `.mcp.json`. +- OpenCode receives `opencode.json` under the `mcp` key. + + +Pi builds emit a warning for MCP servers. The declaration still works for other targets in the same build. + + +Next: [tools](/docs/tools) and the [support matrix](/docs/harnesses). diff --git a/apps/docs/content/docs/meta.json b/apps/docs/content/docs/meta.json index f77e6e7..cd53dce 100644 --- a/apps/docs/content/docs/meta.json +++ b/apps/docs/content/docs/meta.json @@ -6,11 +6,21 @@ "index", "installation", "quickstart", + "---Define---", + "skills", + "commands", + "subagents", + "hooks", + "mcp", + "tools", + "instructions-and-files", "---Guides---", "installing-plugins", "porting", "harnesses", "authoring-a-harness", + "---Reference---", + "api", "---Project---", "origins" ] diff --git a/apps/docs/content/docs/porting.mdx b/apps/docs/content/docs/porting.mdx index 498189a..ffa8c85 100644 --- a/apps/docs/content/docs/porting.mdx +++ b/apps/docs/content/docs/porting.mdx @@ -1,168 +1,58 @@ --- -title: Port a Claude Code plugin -description: Turn an existing Claude Code plugin into one portable definePlugin that ships to every harness. +title: Port an existing plugin +description: Turn a Claude Code plugin, Cursor rules, Copilot prompts, or another native layout into one portable plugin.ts. --- -Already have a Claude Code plugin — a `.claude-plugin/`, `agents/`, `commands/`, -`hooks/`, skills, and companion files? Port it **once** into a portable -`definePlugin` and compile it to Claude Code, Codex, Gemini CLI, Copilot, Cursor, -Windsurf, Pi, and OpenCode. You keep your files on disk; the plugin definition -just points at them. +Already have a plugin or rules folder for one harness? Port it **once** into a portable `definePlugin` and compile it to Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode. You keep your files on disk; the generated plugin definition points at them. ## The fast path: `ap-sdk port` -Don't write it by hand — point the CLI at your existing plugin and it generates a -`plugin.ts` that **loads your files** (never inlines). It auto-detects the source -layout (Claude Code, Codex, Gemini, Copilot, Cursor, Windsurf, OpenCode, Pi, or a -generic `skills/` + `commands/` + `agents/` tree): - ```bash npx ap-sdk port ./my-plugin # writes ./my-plugin/plugin.ts npx ap-sdk port ./my-plugin --dry-run # preview it first ``` -It maps the manifest, instruction file, `**/SKILL.md` skills, commands, agents -(with their `model` and extra frontmatter), and hooks (native event names → -portable ones), and ships companion directories with `readDir`. Then: +`port` detects the source layout, reads manifests, instruction files, `SKILL.md` skills, commands, agents, hooks, and companion directories, then writes a portable `plugin.ts` that loads files with `readTextFrom`, `readBodyFrom`, and `readDir`. ```bash compact -npx ap-sdk check plugin.ts # validate -npx ap-sdk build plugin.ts # emit every harness +npx ap-sdk check plugin.ts +npx ap-sdk build plugin.ts ``` -Review the result and fill in anything the generator flagged with a `TODO`. The -rest of this page explains the mapping it applies, for when you want to tweak by -hand. +Review any generated `TODO` comments. Unmapped native hook events are flagged in code so you can choose the right portable event. -## The mapping +## Source layouts -| Claude Code | ap-sdk | -| --- | --- | -| `.claude-plugin/plugin.json` | `id`, `description`, `version`, `author`, `homepage`, `license`, `marketplace` | -| `CLAUDE.md` | `instructions` | -| skills (`SKILL.md`) | `defineSkill` (extra frontmatter → `frontmatter`) | -| `agents/*.md` | `defineSubagent` (`model` → `harness.claude.model`; extra fields → `frontmatter`) | -| `commands/*.md` | `defineCommand` (extra frontmatter → `frontmatter`) | -| `hooks/hooks.json` | `defineHook` (events incl. `notification`, `permission-request`; `async` preserved) | -| `hooks/*.sh`, `*.py`, `doctrine/`, `docs/` | `files` (companion files, via `readDir`) | +| Source layout | Detector looks for | Guide | +| --- | --- | --- | +| Claude Code plugin | `.claude-plugin/plugin.json`, `CLAUDE.md`, `skills/**/SKILL.md`, `commands/`, `agents/`, `hooks/` | [Claude Code](/docs/porting/claude) | +| Gemini CLI extension | `gemini-extension.json`, `GEMINI.md`, `.gemini/agents` | [Gemini CLI](/docs/porting/gemini) | +| OpenCode plugin | `opencode.json` plus generic content folders | [OpenCode](/docs/porting/opencode) | +| GitHub Copilot | `.github/copilot-instructions.md`, `.github/prompts`, `.github/agents` | [Copilot](/docs/porting/copilot) | +| Cursor | `.cursor` plus generic content folders | [Cursor](/docs/porting/cursor) | +| Windsurf | `.windsurf` plus generic content folders | [Windsurf](/docs/porting/windsurf) | +| Codex | `.codex` or `.agents`, `.codex/prompts` | [Codex](/docs/porting/codex) | +| Pi | `.pi` plus generic content folders | [Pi](/docs/porting/pi) | +| Generic tree | `skills/`, `commands/`, `agents/`, instruction files | [Generic tree](/docs/porting/generic) | ## Keep your files on disk -Instead of inlining prose, load it — `readText` for a single file, -`readTextFrom` to bind a base, `readDir` to ship a whole folder. Resolve paths -relative to the plugin file with `import.meta.url`: - ```ts title="plugin.ts" -import { - definePlugin, - defineSkill, - defineSubagent, - defineCommand, - defineHook, - readText, - readTextFrom, - readDir, -} from "@jalco/ap-sdk"; +import { definePlugin, readDir, readTextFrom } from "@jalco/ap-sdk"; const read = readTextFrom(import.meta.url); export default definePlugin({ id: "alp-river", description: "Multi-step agent refinement pipeline.", - version: "1.3.2", - author: { name: "Alper Ortac" }, instructions: read("./CLAUDE.md"), + files: readDir("./hooks", import.meta.url, "hooks"), }); ``` -## Skills, agents, commands - -Load each body from its file, and use the `frontmatter` escape hatch for native -fields the SDK doesn't model (an agent's `effort`, a nested `stage:` block): - -```ts -skills: [ - defineSkill({ - name: "diff-review", - description: "Summarize and risk-flag uncommitted changes.", - instructions: read("./skills/diff-review/SKILL.md"), - }), -], -subagents: [ - defineSubagent({ - name: "triage", - description: "Classify the task and route it.", - prompt: read("./agents/triage.md"), - tools: ["Read", "Grep", "Bash"], - harness: { claude: { model: "sonnet" } }, - frontmatter: { effort: "high", stage: { routes: ["code"] } }, - }), -], -commands: [ - defineCommand({ - name: "run", - description: "Run the pipeline.", - body: read("./commands/run.md"), - }), -], -``` - -The known fields always win a clash, so passthrough only *adds* frontmatter — it -can't accidentally overwrite `name` or `description`. - -## Hooks and their scripts - -Hook events map to every harness's native name — including `notification` and -`permission-request`. Ship the scripts your hooks call as **companion files**, -and reference them via the harness's plugin-root variable: - -```ts -hooks: [ - defineHook({ - event: "session-start", - command: "${CLAUDE_PLUGIN_ROOT}/hooks/inject-workflow.sh", - timeout: 5, - }), - defineHook({ - event: "notification", - command: "${CLAUDE_PLUGIN_ROOT}/hooks/notify.sh", - async: true, // fire-and-forget; Claude runs it without blocking the turn - }), -], -// Ship the hook scripts + reference docs the instructions point at. -files: [ - ...readDir("./hooks", import.meta.url, "hooks"), - ...readDir("./doctrine", import.meta.url, "doctrine"), -], -``` - -`readDir` preserves the executable bit, so your `.sh`/`.py` hooks stay runnable. -A harness with no native form for an event (or for hooks at all) drops just that -piece with a [warning](/docs/harnesses#what-happens-when-a-feature-isnt-supported), -never a broken file. `async` is emitted where the harness models it natively -(Claude Code) and otherwise dropped with a warning — the hook still runs, just -synchronously within its timeout. - -## Build and review the gaps - -```bash compact -npx ap-sdk check # validate the definition -npx ap-sdk build # emit every harness tree under .aps-out/ -``` - -`build` prints a warning for anything a target couldn't represent — read them to -see exactly what each non-Claude harness dropped, and use each feature's -`harness` override to bridge where it matters. The [support matrix](/docs/harnesses) -shows the capabilities at a glance. - -## Let an agent do it - -Porting is mechanical, so it's a great job for an agent. Point your coding agent -at the **Port a Claude Code plugin** skill (in the repo's `skills/`), which walks -it through inspecting the plugin tree and generating a `plugin.ts` that loads your -files. +A harness with no native form for a feature drops just that piece with a [warning](/docs/harnesses#what-happens-when-a-feature-isnt-supported), never a broken file. - - + + diff --git a/apps/docs/content/docs/porting/claude.mdx b/apps/docs/content/docs/porting/claude.mdx new file mode 100644 index 0000000..9b5a3a2 --- /dev/null +++ b/apps/docs/content/docs/porting/claude.mdx @@ -0,0 +1,35 @@ +--- +title: Port a Claude Code plugin to every harness +description: Convert a Claude Code layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode. +--- + +The detector recognizes this source when it finds `.claude-plugin/plugin.json`, `CLAUDE.md`, `skills/**/SKILL.md`, `commands/*.md`, `agents/*.md`, and `hooks/hooks.json`. + +```bash +npx ap-sdk port ./my-plugin +npx ap-sdk port ./my-plugin --dry-run +``` + +## Mapping + +| Claude Code source | ap-sdk output | +| --- | --- | +| `.claude-plugin/plugin.json` | plugin id, description, version, author | +| `CLAUDE.md` | `instructions` | +| `skills/**/SKILL.md` | `defineSkill` | +| `commands/*.md` | `defineCommand` | +| `agents/*.md` | `defineSubagent` | +| `hooks/hooks.json` | `defineHook` | + +## What does not carry over + +The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`. + +## Next steps + +```bash compact +npx ap-sdk check plugin.ts +npx ap-sdk build plugin.ts +``` + +Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use. diff --git a/apps/docs/content/docs/porting/codex.mdx b/apps/docs/content/docs/porting/codex.mdx new file mode 100644 index 0000000..8990213 --- /dev/null +++ b/apps/docs/content/docs/porting/codex.mdx @@ -0,0 +1,32 @@ +--- +title: Port a Codex plugin to every harness +description: Convert a Codex layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode. +--- + +The detector recognizes this source when it finds `.codex` or `.agents`, plus `.codex/prompts` for prompt commands. + +```bash +npx ap-sdk port ./my-plugin +npx ap-sdk port ./my-plugin --dry-run +``` + +## Mapping + +| Codex source | ap-sdk output | +| --- | --- | +| `.codex/prompts/*.md` | `defineCommand` | +| `.agents` / `agents/*.md` | `defineSubagent` | +| `AGENTS.md` | `instructions` | + +## What does not carry over + +The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`. + +## Next steps + +```bash compact +npx ap-sdk check plugin.ts +npx ap-sdk build plugin.ts +``` + +Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use. diff --git a/apps/docs/content/docs/porting/copilot.mdx b/apps/docs/content/docs/porting/copilot.mdx new file mode 100644 index 0000000..e80501b --- /dev/null +++ b/apps/docs/content/docs/porting/copilot.mdx @@ -0,0 +1,32 @@ +--- +title: Port a GitHub Copilot plugin to every harness +description: Convert a GitHub Copilot layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode. +--- + +The detector recognizes this source when it finds `.github/copilot-instructions.md`, `.github/prompts`, and `.github/agents`. + +```bash +npx ap-sdk port ./my-plugin +npx ap-sdk port ./my-plugin --dry-run +``` + +## Mapping + +| GitHub Copilot source | ap-sdk output | +| --- | --- | +| `.github/copilot-instructions.md` | `instructions` | +| `.github/prompts/*.prompt.md` | `defineCommand` | +| `.github/agents/*.agent.md` | `defineSubagent` | + +## What does not carry over + +The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`. + +## Next steps + +```bash compact +npx ap-sdk check plugin.ts +npx ap-sdk build plugin.ts +``` + +Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use. diff --git a/apps/docs/content/docs/porting/cursor.mdx b/apps/docs/content/docs/porting/cursor.mdx new file mode 100644 index 0000000..81974f3 --- /dev/null +++ b/apps/docs/content/docs/porting/cursor.mdx @@ -0,0 +1,33 @@ +--- +title: Port a Cursor plugin to every harness +description: Convert a Cursor layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode. +--- + +The detector recognizes this source when it finds `.cursor` plus shared `skills/`, `commands/`, `agents/`, hooks, and instruction files. + +```bash +npx ap-sdk port ./my-plugin +npx ap-sdk port ./my-plugin --dry-run +``` + +## Mapping + +| Cursor source | ap-sdk output | +| --- | --- | +| `.cursor` | layout detection | +| `skills/**/SKILL.md` | `defineSkill` | +| `commands/*.md` | `defineCommand` | +| `agents/*.md` | `defineSubagent` | + +## What does not carry over + +The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`. + +## Next steps + +```bash compact +npx ap-sdk check plugin.ts +npx ap-sdk build plugin.ts +``` + +Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use. diff --git a/apps/docs/content/docs/porting/gemini.mdx b/apps/docs/content/docs/porting/gemini.mdx new file mode 100644 index 0000000..4bb7181 --- /dev/null +++ b/apps/docs/content/docs/porting/gemini.mdx @@ -0,0 +1,32 @@ +--- +title: Port a Gemini CLI plugin to every harness +description: Convert a Gemini CLI layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode. +--- + +The detector recognizes this source when it finds `gemini-extension.json`, `GEMINI.md`, and `.gemini/agents/*.md`. + +```bash +npx ap-sdk port ./my-plugin +npx ap-sdk port ./my-plugin --dry-run +``` + +## Mapping + +| Gemini CLI source | ap-sdk output | +| --- | --- | +| `gemini-extension.json` | plugin id, description, version, author | +| `GEMINI.md` | `instructions` | +| `.gemini/agents/*.md` | `defineSubagent` | + +## What does not carry over + +The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`. + +## Next steps + +```bash compact +npx ap-sdk check plugin.ts +npx ap-sdk build plugin.ts +``` + +Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use. diff --git a/apps/docs/content/docs/porting/generic.mdx b/apps/docs/content/docs/porting/generic.mdx new file mode 100644 index 0000000..b3371e5 --- /dev/null +++ b/apps/docs/content/docs/porting/generic.mdx @@ -0,0 +1,33 @@ +--- +title: Port a generic tree plugin to every harness +description: Convert a generic tree layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode. +--- + +The detector recognizes this source when it finds no native marker; falls back to `skills/`, `commands/`, `agents/`, hooks, and instruction files. + +```bash +npx ap-sdk port ./my-plugin +npx ap-sdk port ./my-plugin --dry-run +``` + +## Mapping + +| generic tree source | ap-sdk output | +| --- | --- | +| `skills/**/SKILL.md` | `defineSkill` | +| `commands/*.md` | `defineCommand` | +| `agents/*.md` | `defineSubagent` | +| `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` | `instructions` | + +## What does not carry over + +The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`. + +## Next steps + +```bash compact +npx ap-sdk check plugin.ts +npx ap-sdk build plugin.ts +``` + +Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use. diff --git a/apps/docs/content/docs/porting/meta.json b/apps/docs/content/docs/porting/meta.json new file mode 100644 index 0000000..53857de --- /dev/null +++ b/apps/docs/content/docs/porting/meta.json @@ -0,0 +1,14 @@ +{ + "title": "Porting", + "pages": [ + "claude", + "codex", + "gemini", + "copilot", + "cursor", + "windsurf", + "opencode", + "pi", + "generic" + ] +} diff --git a/apps/docs/content/docs/porting/opencode.mdx b/apps/docs/content/docs/porting/opencode.mdx new file mode 100644 index 0000000..91fcb71 --- /dev/null +++ b/apps/docs/content/docs/porting/opencode.mdx @@ -0,0 +1,32 @@ +--- +title: Port a OpenCode plugin to every harness +description: Convert a OpenCode layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode. +--- + +The detector recognizes this source when it finds `opencode.json` plus shared content folders. + +```bash +npx ap-sdk port ./my-plugin +npx ap-sdk port ./my-plugin --dry-run +``` + +## Mapping + +| OpenCode source | ap-sdk output | +| --- | --- | +| `opencode.json` | plugin id, description, version, author | +| `AGENTS.md` | `instructions` | +| `commands/*.md` | `defineCommand` | + +## What does not carry over + +The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`. + +## Next steps + +```bash compact +npx ap-sdk check plugin.ts +npx ap-sdk build plugin.ts +``` + +Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use. diff --git a/apps/docs/content/docs/porting/pi.mdx b/apps/docs/content/docs/porting/pi.mdx new file mode 100644 index 0000000..c6448c8 --- /dev/null +++ b/apps/docs/content/docs/porting/pi.mdx @@ -0,0 +1,32 @@ +--- +title: Port a Pi plugin to every harness +description: Convert a Pi layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode. +--- + +The detector recognizes this source when it finds `.pi` plus shared content folders and `AGENTS.md`. + +```bash +npx ap-sdk port ./my-plugin +npx ap-sdk port ./my-plugin --dry-run +``` + +## Mapping + +| Pi source | ap-sdk output | +| --- | --- | +| `.pi` | layout detection | +| `AGENTS.md` | `instructions` | +| `skills/**/SKILL.md` | `defineSkill` | + +## What does not carry over + +The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`. + +## Next steps + +```bash compact +npx ap-sdk check plugin.ts +npx ap-sdk build plugin.ts +``` + +Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use. diff --git a/apps/docs/content/docs/porting/windsurf.mdx b/apps/docs/content/docs/porting/windsurf.mdx new file mode 100644 index 0000000..2dc516a --- /dev/null +++ b/apps/docs/content/docs/porting/windsurf.mdx @@ -0,0 +1,33 @@ +--- +title: Port a Windsurf plugin to every harness +description: Convert a Windsurf layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode. +--- + +The detector recognizes this source when it finds `.windsurf` plus shared `skills/`, `commands/`, `agents/`, hooks, and instruction files. + +```bash +npx ap-sdk port ./my-plugin +npx ap-sdk port ./my-plugin --dry-run +``` + +## Mapping + +| Windsurf source | ap-sdk output | +| --- | --- | +| `.windsurf` | layout detection | +| `skills/**/SKILL.md` | `defineSkill` | +| `commands/*.md` | `defineCommand` | +| `agents/*.md` | `defineSubagent` | + +## What does not carry over + +The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`. + +## Next steps + +```bash compact +npx ap-sdk check plugin.ts +npx ap-sdk build plugin.ts +``` + +Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use. diff --git a/apps/docs/content/docs/quickstart.mdx b/apps/docs/content/docs/quickstart.mdx index 96c5ad9..4c46d39 100644 --- a/apps/docs/content/docs/quickstart.mdx +++ b/apps/docs/content/docs/quickstart.mdx @@ -5,7 +5,13 @@ description: Define a plugin and compile it to every harness in a few minutes. ## 1. Define a plugin -Create `plugin.ts` and default-export the result of `definePlugin`: +Start with a working scaffold: + +```bash +npx ap-sdk init my-plugin +``` + +That writes a `plugin.ts` you can edit. Or create `plugin.ts` yourself and default-export the result of `definePlugin`: ```ts title="plugin.ts" import { @@ -76,6 +82,12 @@ npx ap-sdk build -t claude,gemini,cursor npx ap-sdk install -t claude ``` +For an edit/build/install loop while authoring, run watch mode: + +```bash +npx ap-sdk dev --install -t claude +``` + Add `--global` to install into your home-directory harness dirs, or `--dry-run` to preview without writing. See [Installing a plugin](/docs/installing-plugins) for the full install model. diff --git a/apps/docs/content/docs/skills.mdx b/apps/docs/content/docs/skills.mdx new file mode 100644 index 0000000..284a253 --- /dev/null +++ b/apps/docs/content/docs/skills.mdx @@ -0,0 +1,47 @@ +--- +title: Skills +description: Define reusable skill documents once and emit native skill folders for every harness. +--- + +Skills are reusable instructions with a short trigger description. The SDK emits each skill into the native skill format a harness understands and warns when a harness cannot represent an option. + +```ts title="plugin.ts" +import { definePlugin, defineSkill } from "@jalco/ap-sdk"; + +export default definePlugin({ + id: "review-helper", + description: "Git review workflows.", + skills: [ + defineSkill({ + name: "diff-review", + description: + "Use when the user asks what changed or wants risks before committing.", + instructions: "Run `git diff HEAD`, summarize changes, then list risks.", + allowedTools: ["Bash", "Read", "Grep"], + resources: [{ path: "checklist.md", content: "# Review checklist +" }], + frontmatter: { color: "blue" }, + }), + ], +}); +``` + +## Fields + +- `name` — kebab-case id, max 64 characters. +- `description` — max 1024 characters, the tightest harness limit; front-load trigger conditions because agents use this to decide when to load the skill. +- `instructions` — the skill body. +- `allowedTools` — honored by Claude Code and Pi. +- `disableModelInvocation` — Pi-specific. +- `license` — emitted where native skill metadata supports it. +- `metadata` — honored by OpenCode and Pi. +- `frontmatter` — escape hatch merged into YAML-frontmatter harnesses; SDK-owned fields win on conflicts. +- `resources` — extra files emitted inside the skill directory. + + +Keep descriptions concise and action-oriented. The first sentence should say when to use the skill. + + +## Portability notes + +Unsupported options become structured build warnings; they are not silently dropped. See the [support matrix](/docs/harnesses), then continue with [commands](/docs/commands). diff --git a/apps/docs/content/docs/subagents.mdx b/apps/docs/content/docs/subagents.mdx new file mode 100644 index 0000000..b422981 --- /dev/null +++ b/apps/docs/content/docs/subagents.mdx @@ -0,0 +1,44 @@ +--- +title: Subagents +description: Define specialist agents with prompts, tools, and per-harness model hints. +--- + +Subagents package a focused prompt under a name so the primary agent can delegate. Claude and OpenCode receive markdown agents, Codex receives developer instructions, and unsupported harnesses emit warnings. + +```ts title="plugin.ts" +import { definePlugin, defineSubagent } from "@jalco/ap-sdk"; + +export default definePlugin({ + id: "review-team", + description: "Specialist review agents.", + subagents: [ + defineSubagent({ + name: "security-reviewer", + description: "Reviews diffs for auth, secrets, and injection risks.", + prompt: "You are a security reviewer. Lead with blockers.", + tools: ["Read", "Grep", "Bash"], + harness: { + claude: { model: "sonnet" }, + codex: { model: "gpt-5" }, + opencode: { model: "gpt-5", mode: "subagent" }, + gemini: { model: "gemini-3-pro", temperature: 0.2, maxTurns: 8 }, + copilot: { model: "gpt-5" }, + }, + }), + ], +}); +``` + +## Fields + +- `name` and `description` identify and route the agent. +- `prompt` maps to Claude/OpenCode body text and Codex `developer_instructions`. +- `tools` is honored by Claude Code. +- `frontmatter` is emitted for YAML-frontmatter harnesses; Codex TOML ignores it. +- `harness` sets target-specific model and behavior hints, including Gemini `temperature` / `maxTurns` and OpenCode `mode` (`primary`, `subagent`, `all`). + + +Pi has no native subagent artifact, so builds for Pi report a structured warning rather than inventing a broken file. + + +Next: [hooks](/docs/hooks) and the [support matrix](/docs/harnesses). diff --git a/apps/docs/content/docs/tools.mdx b/apps/docs/content/docs/tools.mdx new file mode 100644 index 0000000..c15b554 --- /dev/null +++ b/apps/docs/content/docs/tools.mdx @@ -0,0 +1,45 @@ +--- +title: Tools +description: Author one TypeScript tool module and generate harness glue around it. +--- + +Tools let you keep real implementation code in one `tools.ts` module. The build copies the module into every output and generates the glue each harness needs: MCP server wiring for Claude/Codex and native tool/plugin glue for Pi/OpenCode. + +```ts title="tools.ts" +import { defineTool } from "@jalco/ap-sdk/runtime"; + +export default [ + defineTool({ + name: "echo", + description: "Echo text back for smoke tests.", + inputSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] }, + async execute(input: { text: string }) { + return { content: [{ type: "text", text: input.text }] }; + }, + }), +]; +``` + +```ts title="plugin.ts" +import { definePlugin } from "@jalco/ap-sdk"; + +export default definePlugin({ + id: "echo-tool", + description: "Tool smoke test.", + tools: { module: "./tools.ts", names: ["echo"] }, +}); +``` + +## Local loop + +```bash +ap-sdk tools --call echo --args '{"text":"hello"}' +``` + +## Fields + +- `tools.module` points at the TypeScript module. +- `tools.names` limits or documents the exported tools. +- `defineTool` describes the name, description, input schema, and execute function. + +See `packages/agent-plugin-sdk/examples/echo-tool/` and `examples/planreview/` for working projects. Next: [instructions and files](/docs/instructions-and-files) and the [support matrix](/docs/harnesses). diff --git a/apps/docs/package.json b/apps/docs/package.json index c2e278f..37ac62a 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -9,7 +9,9 @@ "typecheck": "fumadocs-mdx && next typegen && tsc --noEmit", "postinstall": "fumadocs-mdx", "lint": "biome check", - "format": "biome format --write" + "format": "biome format --write", + "generate:api": "node scripts/generate-api-docs.mjs", + "prebuild": "pnpm generate:api" }, "dependencies": { "@jalco/ap-sdk": "workspace:*", diff --git a/apps/docs/scripts/generate-api-docs.mjs b/apps/docs/scripts/generate-api-docs.mjs new file mode 100644 index 0000000..c9686f4 --- /dev/null +++ b/apps/docs/scripts/generate-api-docs.mjs @@ -0,0 +1,142 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const outDir = resolve(process.cwd(), "content/docs/api"); +mkdirSync(outDir, { recursive: true }); + +const groups = [ + { + slug: "main", + title: "Main entrypoint", + description: "Public helpers and types exported by @jalco/ap-sdk.", + symbols: [ + [ + "definePlugin", + "Identity helper for a portable plugin definition. Author once, then build or install native artifacts for each harness.", + "export function definePlugin(plugin: Plugin): Plugin;", + ], + [ + "defineSkill", + "Defines a portable agent skill with a trigger description, instructions, optional resources, and harness metadata.", + "export function defineSkill(skill: Skill): Skill;", + ], + [ + "defineCommand", + "Defines a portable slash command or prompt command with argument templating and per-harness overrides.", + "export function defineCommand(command: Command): Command;", + ], + [ + "defineSubagent", + "Defines a specialist agent prompt with optional tools and target-specific model settings.", + "export function defineSubagent(subagent: Subagent): Subagent;", + ], + [ + "defineHook", + "Defines a lifecycle hook using ap-sdk's portable event names and optional native overrides.", + "export function defineHook(hook: Hook): Hook;", + ], + [ + "Plugin", + "The root declaration: id, description, instructions, skills, commands, MCP servers, subagents, hooks, files, tools, and marketplace metadata.", + "export interface Plugin { id: string; description: string; ... }", + ], + [ + "Skill", + "Reusable instructions plus metadata. Descriptions are capped to the tightest harness limit and drive skill routing.", + "export interface Skill { name: string; description: string; instructions: string; ... }", + ], + [ + "Hook", + "Portable hook event, matcher, command, timeout, async flag, and per-harness event or matcher overrides.", + "export interface Hook { event: HookEvent; command: string | HookCommand; ... }", + ], + [ + "build", + "Validates a plugin and returns in-memory output files and warnings for each requested harness.", + "export function build(plugin: Plugin, options?: BuildOptions): HarnessBuild[];", + ], + [ + "installSkills", + "Installs emitted skills, commands, MCP config, hooks, instructions, subagents, and files into live harness directories.", + "export function installSkills(plugin: Plugin, options?: InstallOptions): InstalledItem[];", + ], + ], + }, + { + slug: "runtime", + title: "Runtime entrypoint", + description: "Runtime helpers for portable tools.", + symbols: [ + [ + "defineTool", + "Defines one executable tool with metadata, JSON schema parameters, and a single handler shared across harness adapters.", + "export function defineTool(tool: Tool): Tool;", + ], + [ + "listTools", + "Loads a tools module and returns the declared tool metadata for local inspection or generated adapters.", + "export async function listTools(modulePath: string): Promise;", + ], + [ + "callTool", + "Invokes a named tool locally with JSON arguments, matching the CLI's `ap-sdk tools --call` loop.", + "export async function callTool(modulePath: string, name: string, args: unknown): Promise;", + ], + [ + "contentToText", + "Converts structured tool content blocks into plain text for terminal output.", + "export function contentToText(content: ToolContent[]): string;", + ], + ], + }, + { + slug: "harness", + title: "Harness entrypoint", + description: "Harness authoring toolkit for custom targets.", + symbols: [ + [ + "defineHarness", + "Identity helper for a custom target harness: support map, emit translator, install paths, and native config behavior.", + "export function defineHarness(harness: Harness): Harness;", + ], + [ + "registerHarness", + "Registers a custom harness id so build, install, and CLI target validation can use it.", + "export function registerHarness(harness: Harness): void;", + ], + [ + "Harness", + "The contract every built-in and custom harness implements: id, display name, support map, emit function, and install path helpers.", + "export interface Harness { id: HarnessId; displayName: string; supports: FeatureSupport; ... }", + ], + [ + "EmitContext", + "Context passed to emitters, including helper methods and target metadata.", + "export interface EmitContext { harness: Harness; ... }", + ], + [ + "InstallScope", + "Where install writes artifacts: project directories or home-directory global config.", + "export type InstallScope = 'project' | 'global';", + ], + ], + }, +]; + +for (const group of groups) { + const body = group.symbols + .map( + ([name, description, signature]) => + `## ${name}\n\n${description}\n\n\`\`\`ts\n${signature}\n\`\`\``, + ) + .join("\n\n"); + writeFileSync( + resolve(outDir, `${group.slug}.mdx`), + `---\ntitle: ${group.title}\ndescription: ${group.description}\n---\n\n${body}\n`, + ); +} + +writeFileSync( + resolve(outDir, "index.mdx"), + `---\ntitle: API reference\ndescription: Generated reference for the public ap-sdk entrypoints.\n---\n\nThis section is generated from source-aligned API metadata. Regenerate it with \`pnpm --filter @jal-co/docs generate:api\`.\n\n\n \n \n \n \n\n`, +); diff --git a/apps/docs/src/app/docs/[[...slug]]/page.tsx b/apps/docs/src/app/docs/[[...slug]]/page.tsx index ac55955..6419ceb 100644 --- a/apps/docs/src/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/src/app/docs/[[...slug]]/page.tsx @@ -2,7 +2,8 @@ import { getGithubLastEdit } from "fumadocs-core/content/github"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { AiCopyButton } from "@/components/ai-copy-button"; -import { DocsToc } from "@/components/docs-toc"; +import { DocsPagination } from "@/components/docs-pagination"; +import { DocsToc, MobileDocsToc } from "@/components/docs-toc"; import { getMDXComponents } from "@/components/mdx"; import { gitConfig } from "@/lib/shared"; import { getLLMText, getPageImage, source } from "@/lib/source"; @@ -34,6 +35,7 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) { const toc = page.data.toc; const pageText = await getLLMText(page); const lastEdit = await getLastEdit(page.path); + const editUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}/edit/${gitConfig.branch}/apps/docs/content/docs/${page.path}`; return (
@@ -50,9 +52,31 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) { ) : null}
+ +
+ + @@ -80,6 +104,14 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) { )}

) : null} + + Edit this page on GitHub + diff --git a/apps/docs/src/app/docs/layout.tsx b/apps/docs/src/app/docs/layout.tsx index 5392d6f..49e7713 100644 --- a/apps/docs/src/app/docs/layout.tsx +++ b/apps/docs/src/app/docs/layout.tsx @@ -8,7 +8,7 @@ export default function Layout({ children }: LayoutProps<"/docs">) { return (
- +
); } + +/** Mobile: collapsible page-local table of contents. */ +export function MobileDocsToc({ items }: { items: TOCItemType[] }) { + const [open, setOpen] = useState(false); + + if (!items.length) return null; + + return ( +
+ +
+
+
+ +
+
+
+
+ ); +} diff --git a/apps/docs/src/components/mdx.tsx b/apps/docs/src/components/mdx.tsx index 7af951c..7a7fb24 100644 --- a/apps/docs/src/components/mdx.tsx +++ b/apps/docs/src/components/mdx.tsx @@ -1,10 +1,12 @@ import type { MDXComponents } from "mdx/types"; import Link from "next/link"; import { type ComponentProps, isValidElement, type ReactNode } from "react"; +import { Callout as CalloutBase } from "@/components/callout"; import { CodeBlock as CodeBlockBase } from "@/components/code-block"; import { CodeBlockCommand as CodeBlockCommandBase } from "@/components/code-block-command"; import { CodeLine as CodeLineBase } from "@/components/code-line"; import { SupportMatrix } from "@/components/support-matrix"; +import { Tab as TabBase, Tabs as TabsBase } from "@/components/tabs"; import { convertNpmCommand } from "@/lib/convert-npm-command"; import { cn } from "@/lib/utils"; @@ -43,6 +45,14 @@ function CodeBlockCommand({ ); } +function Callout({ className, ...props }: ComponentProps) { + return ; +} + +function Tabs({ className, ...props }: ComponentProps) { + return ; +} + /** * Tabbed install/CLI command block. Author one npm-style command; the other * package managers are derived so docs never drift between managers. @@ -168,6 +178,9 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents { CodeBlock, CodeLine, CodeBlockCommand, + Callout, + Tabs, + Tab: TabBase, NpmCommand, SupportMatrix, ...components, diff --git a/apps/docs/src/components/plugin-directory.tsx b/apps/docs/src/components/plugin-directory.tsx new file mode 100644 index 0000000..60c58f9 --- /dev/null +++ b/apps/docs/src/components/plugin-directory.tsx @@ -0,0 +1,229 @@ +"use client"; + +import { Check, Copy, Star } from "lucide-react"; +import { useMemo, useState } from "react"; +import type { PluginChannel, PluginEntry } from "@/lib/github-plugins"; + +const NPM_INSTALL_SUPPORTED = true; + +function relativeTime(value: string): string { + const diff = new Date(value).getTime() - Date.now(); + const days = Math.round(diff / 86_400_000); + const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); + if (Math.abs(days) < 1) return "today"; + if (Math.abs(days) < 60) return rtf.format(days, "day"); + const months = Math.round(days / 30); + if (Math.abs(months) < 24) return rtf.format(months, "month"); + return rtf.format(Math.round(days / 365), "year"); +} + +function CopyCommand({ command }: { command: string }) { + const [copied, setCopied] = useState(false); + return ( + + ); +} + +function ChannelBadge({ channel }: { channel: PluginChannel }) { + return ( + + {channel === "github" ? "git" : "npm"} + + ); +} + +function PluginCard({ entry }: { entry: PluginEntry }) { + const href = + entry.repoUrl ?? `https://www.npmjs.com/package/${entry.npmName}`; + return ( +
+
+ {entry.avatarUrl ? ( + // GitHub avatar domains are dynamic; keep this tiny decorative image unoptimized. + // biome-ignore lint/performance/noImgElement: external avatar URL from registry data + + ) : null} +
+ + {entry.name} + +

{entry.owner}

+
+ {entry.stars !== null ? ( + + {entry.stars} + + ) : null} +
+

+ {entry.description ?? "No description provided."} +

+
+ {entry.channels.map((channel) => ( + + ))} + + updated {relativeTime(entry.updatedAt)} + + {entry.topics + .filter((t) => t !== "ap-sdk-plugin") + .slice(0, 3) + .map((topic) => ( + + {topic} + + ))} +
+
+ {entry.fullName && entry.channels.includes("github") ? ( + + ) : null} + {entry.npmName ? ( + NPM_INSTALL_SUPPORTED ? ( + + ) : ( + + View npm package + + ) + ) : null} +
+
+ ); +} + +export function PluginDirectory({ + entries, + degraded, +}: { + entries: PluginEntry[]; + degraded: boolean; +}) { + const [query, setQuery] = useState(""); + const [channel, setChannel] = useState<"all" | PluginChannel>("all"); + const [sort, setSort] = useState<"stars" | "recent" | "az">("stars"); + + const filtered = useMemo(() => { + const q = query.toLowerCase().trim(); + return entries + .filter((entry) => channel === "all" || entry.channels.includes(channel)) + .filter( + (entry) => + !q || + [entry.name, entry.description ?? "", entry.owner].some((v) => + v.toLowerCase().includes(q), + ), + ) + .sort((a, b) => { + if (sort === "az") return a.name.localeCompare(b.name); + if (sort === "recent") + return ( + new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() + ); + return (b.stars ?? -1) - (a.stars ?? -1); + }); + }, [channel, entries, query, sort]); + + if (degraded) { + return ( +
+ Couldn't reach GitHub or npm right now. Browse the{" "} + + ap-sdk-plugin topic + {" "} + directly. +
+ ); + } + + if (entries.length === 0) { + return ( +
+

+ No plugins tagged yet +

+

+ Be the first: tag your repo with ap-sdk-plugin or publish + an npm package with that keyword. +

+
+ ); + } + + return ( +
+
+ setQuery(event.target.value)} + placeholder="Filter by name, description, or author" + className="min-w-0 flex-1 rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/50" + /> + + +
+ {filtered.length === 0 ? ( +
+ No plugins match “{query}”. +
+ ) : ( +
+ {filtered.map((entry) => ( + + ))} +
+ )} +
+ ); +} diff --git a/apps/docs/src/components/search-dialog.tsx b/apps/docs/src/components/search-dialog.tsx new file mode 100644 index 0000000..6967e93 --- /dev/null +++ b/apps/docs/src/components/search-dialog.tsx @@ -0,0 +1,221 @@ +"use client"; + +import { useDocsSearch } from "fumadocs-core/search/client"; +import { Search } from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { Dialog } from "radix-ui"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +/** + * jalco-ui + * SearchDialog + * by Justin Levine + * ui.justinlevine.me + * + * Hand-rolled docs search UI powered by fumadocs-core's search client. + */ + +interface SearchResult { + id: string; + url: string; + type: "page" | "heading" | "text"; + content: string; + breadcrumbs?: string[]; +} + +function plain(value: string): string { + return value + .replace(/<[^>]*>/g, "") + .replace(/\s+/g, " ") + .trim(); +} + +function resultTitle(result: SearchResult): string { + const crumbs = result.breadcrumbs?.map(plain).filter(Boolean) ?? []; + if (crumbs.length) return crumbs.join(" / "); + return plain(result.content) || result.url; +} + +export function SearchTrigger({ + onClick, + mobile = false, +}: { + onClick: () => void; + mobile?: boolean; +}) { + if (mobile) { + return ( + + ); + } + + return ( + + ); +} + +export function SearchDialog({ + open, + onOpenChange, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + const inputRef = useRef(null); + const [active, setActive] = useState(0); + const { search, setSearch, query } = useDocsSearch({ + type: "fetch", + api: "/api/search", + delayMs: 80, + }); + + const results = useMemo( + () => (Array.isArray(query.data) ? (query.data as SearchResult[]) : []), + [query.data], + ); + + useEffect(() => { + if (open) requestAnimationFrame(() => inputRef.current?.focus()); + }, [open]); + + const go = (result: SearchResult | undefined) => { + if (!result) return; + window.location.href = result.url; + onOpenChange(false); + }; + + return ( + + + + + Search documentation +
+ + { + setSearch(event.target.value); + setActive(0); + }} + onKeyDown={(event) => { + if (event.key === "ArrowDown") { + event.preventDefault(); + setActive((i) => Math.min(results.length - 1, i + 1)); + } else if (event.key === "ArrowUp") { + event.preventDefault(); + setActive((i) => Math.max(0, i - 1)); + } else if (event.key === "Enter") { + event.preventDefault(); + go(results[active]); + } + }} + placeholder="Search docs…" + className="min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" + /> + + Esc + +
+
+ {!search ? ( +

+ Type a keyword, command, or harness name. +

+ ) : query.isLoading ? ( +

+ Searching… +

+ ) : query.error ? ( +

+ Search failed. Try again. +

+ ) : results.length === 0 ? ( +

+ No results for “{search}”. +

+ ) : ( +
+ {results.map((result, index) => ( + onOpenChange(false)} + onMouseEnter={() => setActive(index)} + className={cn( + "rounded-xl px-3 py-2 text-sm outline-none transition-colors", + index === active + ? "bg-accent text-foreground" + : "text-foreground hover:bg-accent/60", + )} + > + + {resultTitle(result)} + + + {plain(result.content)} + + + ))} +
+ )} +
+
+
+
+ ); +} + +export function DocsSearch() { + const [open, setOpen] = useState(false); + const pathname = usePathname(); + + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { + event.preventDefault(); + setOpen(true); + } + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, []); + + useEffect(() => { + if (pathname) setOpen(false); + }, [pathname]); + + return ( + <> + setOpen(true)} /> +
+ setOpen(true)} /> +
+ + + ); +} diff --git a/apps/docs/src/components/site-footer.tsx b/apps/docs/src/components/site-footer.tsx index 0c0e305..3775d6b 100644 --- a/apps/docs/src/components/site-footer.tsx +++ b/apps/docs/src/components/site-footer.tsx @@ -4,6 +4,7 @@ import { ComponentCredits } from "@/components/component-credits"; const links = [ { label: "Docs", href: "/docs" }, { label: "Harnesses", href: "/docs/harnesses" }, + { label: "Plugins", href: "/docs/plugins" }, { label: "Changelog", href: "/docs/changelog" }, { label: "GitHub", href: "https://github.com/jal-co/agent-plugin-sdk" }, ]; diff --git a/apps/docs/src/components/site-navbar.tsx b/apps/docs/src/components/site-navbar.tsx index 05d983e..ead2470 100644 --- a/apps/docs/src/components/site-navbar.tsx +++ b/apps/docs/src/components/site-navbar.tsx @@ -1,45 +1,15 @@ "use client"; -/* ───────────────────────────────────────────────────────── - * SCROLL STORYBOARD — Navbar (floating variant) - * - * Driven by scroll position, not a timeline. Two resting states - * the bar springs between as you cross the threshold. - * - * scrollY ≤ 24px "top" full-width bar, square, flush, no chrome - * scrollY > 24px "floating" large centered pill: narrower, fully - * rounded, dropped 12px, blurred card - * surface with a border + shadow - * - * The "docs" variant opts out of this entirely: a flush, full-width - * header bar so the documentation reads like a product surface. - * ───────────────────────────────────────────────────────── */ - -import { motion, useMotionValueEvent, useScroll } from "motion/react"; import Image from "next/image"; import Link from "next/link"; -import { useState } from "react"; +import { DocsSearch } from "@/components/search-dialog"; import { ThemeToggle } from "@/components/theme-toggle"; import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; - -const SCROLL = { - threshold: 24, // px scrolled before the bar collapses into a pill -}; - -/* The morphing shell. Each key holds [top, floating] values. */ -const SHELL = { - maxWidth: [1120, 880], // px — full container → pill width - radius: [0, 999], // border-radius - offsetY: [0, 12], // px drop from the top edge - paddingX: [24, 18], // horizontal padding - paddingY: [14, 10], // vertical padding - spring: { type: "spring" as const, stiffness: 320, damping: 32 }, -}; const LINKS = [ { label: "Docs", href: "/docs" }, { label: "Harnesses", href: "/docs/harnesses" }, + { label: "Plugins", href: "/docs/plugins" }, { label: "Changelog", href: "/docs/changelog" }, ]; @@ -50,7 +20,6 @@ function Logo() { className="group relative flex items-center" aria-label="ap-sdk home" > - {/* Pure-black monochrome mark — invert it in dark mode so it stays visible. */} ap-sdk - {/* On hover the mark dissolves into a terminal-style wordmark. */}