diff --git a/.agents/skills/plugin-creator/SKILL.md b/.agents/skills/plugin-creator/SKILL.md index dde5779e..af960ec5 100644 --- a/.agents/skills/plugin-creator/SKILL.md +++ b/.agents/skills/plugin-creator/SKILL.md @@ -1,6 +1,6 @@ --- name: plugin-creator -description: Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, or generate or update marketplace entries for plugin ordering and availability metadata. +description: Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow. --- # Plugin Creator @@ -10,11 +10,11 @@ description: Create and scaffold plugin directories for Codex with a required `. 1. Run the scaffold script: ```bash - # Plugin names are normalized to lower-case hyphen-case and must be <= 64 chars. - # The generated folder and plugin.json name are always the same. -# Run from repo root (or replace .agents/... with the absolute path to this SKILL). -# By default creates in ~/plugins/. -python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py +# Plugin names are normalized to lower-case hyphen-case and must be <= 64 chars. +# The generated folder and plugin.json name are always the same. +# Run from the skill root (the directory containing this `SKILL.md`). +# By default creates in `~/plugins/`. +python3 scripts/create_basic_plugin.py ``` 2. Edit `/.codex-plugin/plugin.json` when the request gives specific metadata. @@ -23,40 +23,66 @@ python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py ` when the default `personal` marketplace name is already +taken or installed and you need to seed a different new marketplace file: + +```bash +python3 scripts/create_basic_plugin.py my-plugin \ + --with-marketplace \ + --marketplace-name team-local ``` Only use a repo/team marketplace when the user specifically asks for that destination: ```bash -python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py my-plugin \ +python3 scripts/create_basic_plugin.py my-plugin \ --path /plugins \ --marketplace-path /.agents/plugins/marketplace.json \ --with-marketplace ``` +When the user specifies a marketplace path, make sure that marketplace is actually installed before +telling the user to reinstall from it. The default personal marketplace file at +`~/.agents/plugins/marketplace.json` is discovered implicitly, but other marketplace paths are not. +On Windows, use the equivalent path under the user profile. + 4. Generate/adjust optional companion folders as needed: ```bash -python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py my-plugin \ +python3 scripts/create_basic_plugin.py my-plugin \ --path \ --marketplace-path \ --with-skills --with-hooks --with-scripts --with-assets --with-mcp --with-apps --with-marketplace ``` -`` is the directory where the plugin folder `` will be created (for example `~/code/plugins`). +`` is the directory where the plugin folder `` will be +created (for example `~/plugins`). 5. Before handing back a generated plugin, run: ```bash -python3 .agents/skills/plugin-creator/scripts/validate_plugin.py +python3 scripts/validate_plugin.py ``` +For updates to an existing local plugin during development, keep the scaffold flow as-is and use the +reference instead of hand-editing marketplace files: + +```bash +python3 scripts/update_plugin_cachebuster.py +``` + +Prefer the helper default cachebuster unless the user explicitly asks for a specific override. +See `references/installing-and-updating.md` for the expected cachebuster and reinstall flow while iterating on an existing local plugin. + ## What this skill creates -- Default marketplace-backed scaffolds are personal: `~/plugins//` plus - `~/.agents/plugins/marketplace.json`. +- Default marketplace-backed scaffolds use the personal marketplace file at + `~/.agents/plugins/marketplace.json`, with plugins generally being stored in + `~/plugins//`. - Creates plugin root at `///`. - Always creates `///.codex-plugin/plugin.json`. - Fills the manifest with the validated schema shape that the ingestion path accepts. @@ -77,9 +103,18 @@ python3 .agents/skills/plugin-creator/scripts/validate_plugin.py ## Marketplace workflow -- Personal creation defaults to `~/.agents/plugins/marketplace.json`. +- Personal-marketplace creation defaults to `~/.agents/plugins/marketplace.json`. Here, + "personal marketplace" means the marketplace whose file is at that path. - Repo/team marketplace creation is opt-in through both `--path` and `--marketplace-path`, only when the user specifically requests it. +- `--marketplace-name` is an exception path. Use it only when the default `personal` marketplace + name is already taken and you need to seed a different new marketplace file. +- Do not use `--marketplace-name` to rename an existing marketplace file in place. If the file + already exists, its top-level `name` must already match. +- If the user specifies a different marketplace path, treat that marketplace as needing explicit installation via `codex plugin marketplace add`. +- Prefer `scripts/read_marketplace_name.py` when you need the marketplace name from any + `marketplace.json` file. With no argument it reads the default personal marketplace; with an + explicit path it works for repo/team marketplaces too. - In either location, the generated source path remains `./plugins/`. - Marketplace root metadata supports top-level `name` plus optional `interface.displayName`. - Treat plugin order in `plugins[]` as render order in Codex. Append new entries unless a user explicitly asks to reorder the list. @@ -157,6 +192,21 @@ python3 .agents/skills/plugin-creator/scripts/validate_plugin.py - When generating marketplace entries, always write `policy.installation`, `policy.authentication`, and `category` even if their values are defaults. - Add `policy.products` only when the user explicitly asks for that override. - Keep marketplace `source.path` relative to the selected marketplace root as `./plugins/`. +- Only use `--marketplace-name` when creating a new marketplace file whose name should not be + `personal` because that name is already taken or installed elsewhere. +- If Codex would need approval to write the marketplace file, ask for that approval before + proceeding. If the user prefers to run the write themselves, provide the exact scaffold command + and then continue from validation or subsequent plugin edits instead of leaving the workflow + vague. +- For updates to an existing local plugin during development, do not hand-edit marketplace config + or `marketplace.json`. Use the update flow documented in + `references/installing-and-updating.md` and `scripts/update_plugin_cachebuster.py`. +- Do not tell the user to run `codex plugin marketplace add` for the default personal-marketplace + flow. That command is for explicit non-default marketplace configuration, not for the standard + `~/.agents/plugins/marketplace.json` path. +- If the user provided a non-default `--marketplace-path`, make sure that marketplace is installed + before giving reinstall instructions. Use `codex plugin marketplace add ` + when that explicit marketplace has not been configured yet. - When the workflow created or updated a marketplace-backed plugin, end the final user-facing response with a short Codex app handoff. Say `To view this in the Codex app:` and write `View ` and `Share ` as Markdown links, not raw @@ -175,17 +225,19 @@ python3 .agents/skills/plugin-creator/scripts/validate_plugin.py For the exact canonical sample JSON for both plugin manifests and marketplace entries, use: - `references/plugin-json-spec.md` +- `references/installing-and-updating.md` for update/reinstall guidance while + iterating on an existing local plugin, plus the new-thread pickup behavior after reinstall ## Validation After editing `SKILL.md`, run: ```bash -python3 /scripts/quick_validate.py .agents/skills/plugin-creator +python3 ../skill-creator/scripts/quick_validate.py . ``` Before handing back a generated plugin, run: ```bash -python3 .agents/skills/plugin-creator/scripts/validate_plugin.py +python3 scripts/validate_plugin.py ``` diff --git a/.agents/skills/plugin-creator/references/installing-and-updating.md b/.agents/skills/plugin-creator/references/installing-and-updating.md new file mode 100644 index 00000000..28b3b889 --- /dev/null +++ b/.agents/skills/plugin-creator/references/installing-and-updating.md @@ -0,0 +1,143 @@ +# Updating Existing Local Plugins + +Use this reference when a plugin already exists and the request is about updating the plugin during +local development. + +All scripts here are specified relative to the skill root. Update the path for running the scripts +depending on your current working directory. + +## When To Use This Flow + +Use this flow when all of the following are true: + +- the plugin already exists locally +- the marketplace entry already points at the plugin source you are editing +- the user wants Codex to see the updated plugin without manually editing marketplace files + +If the user still needs the initial plugin entry or marketplace structure created, use the scaffold +flow first and only then switch to this reinstall flow. + +## Update Loop + +1. Update the plugin manifest to a single Codex cachebuster suffix: + +```bash +python3 scripts/update_plugin_cachebuster.py \ + +``` + +Prefer the default helper behavior here. If you omit `--cachebuster`, the helper uses a UTC +timestamp down to seconds, which is the recommended path for routine local iteration. + +Only use a manual cachebuster override when the user explicitly asks for one or when a workflow +outside Codex depends on a specific token: + +```bash +python3 scripts/update_plugin_cachebuster.py \ + \ + --cachebuster local-20260519-184516 +``` + +2. For the default scaffolded flow, read the marketplace name from the personal marketplace file: + +```bash +python3 scripts/read_marketplace_name.py +``` + +Here, "personal marketplace" means the marketplace whose file is at +`~/.agents/plugins/marketplace.json`. On Windows, use the equivalent path under the user profile. +The helper uses Python's home-directory resolution and prints the marketplace name to use when +constructing the install command. + +To read the name from a different marketplace file, pass the path directly: + +```bash +python3 scripts/read_marketplace_name.py --marketplace-path +``` + +3. Reinstall from that marketplace name: + +```bash +codex plugin add @ +``` + +The default personal marketplace is discovered implicitly from +`~/.agents/plugins/marketplace.json`. You do not need `codex plugin marketplace add` for that +path, and `codex plugin marketplace list` is not the right check for whether that default +marketplace exists. + +4. If the plugin is not using the personal marketplace file, check which configured local + marketplace is actually surfacing that plugin: + +```bash +codex plugin list +``` + +If the plugin is not in the personal marketplace file, confirm which marketplace entry points at +the plugin source you are editing and make sure that marketplace is still local. If it is a +different local marketplace, reinstall from that marketplace name instead of forcing the personal +marketplace flow. If it is not local, stop and help the user resolve the mismatch before +continuing. + +5. If the plugin lives in a different confirmed local marketplace, substitute that marketplace + name: + +```bash +codex plugin add @ +``` + +6. Prompt the user to use a new thread to try the updated plugin, so that Codex picks up new skills + and tools. + +## Cachebuster Policy + +- Preserve the existing version prefix and replace only the suffix. +- Treat the preserved prefix as everything before `+`. +- Use the format: + +```text ++codex. +``` + +Examples: + +- `0.1.0` → `0.1.0+codex.local-20260519-184516` +- `0.1.0+codex.old-token` → `0.1.0+codex.local-20260519-184516` +- `1.2.3-beta.1+codex.prev` → `1.2.3-beta.1+codex.local-20260519-184516` +- `dev-build+other-tag` → `dev-build+codex.local-20260519-184516` + +Replace the existing Codex cachebuster instead of appending another one. Do not keep incrementing +numeric version components just to trigger reinstall behavior. + +## Marketplace Rules + +- Marketplace manipulation should happen through commands, not by hand-editing `marketplace.json` + or `config.toml` during this update/reinstall flow. +- Prefer the personal marketplace file for the default scaffolded flow. +- Read the personal marketplace name with + `python3 scripts/read_marketplace_name.py` and use the printed value when constructing + `codex plugin add @`. +- For non-default marketplace files, use + `python3 scripts/read_marketplace_name.py --marketplace-path ` to read + the name before constructing reinstall commands. +- Do not tell the user to run `codex plugin marketplace add` for the default personal-marketplace + flow. That marketplace is discovered implicitly by Codex. +- If the user specified a different marketplace path, make sure that marketplace is installed + before giving install or reinstall instructions. Non-default marketplace paths are not + discovered implicitly. +- Use `codex plugin list` when the plugin lives in a different configured marketplace and you need + to confirm which marketplace is surfacing that plugin. +- If a non-default local marketplace has not been configured yet, install it with + `codex plugin marketplace add ` before telling the user to run + `codex plugin add @`. +- If the plugin is not in the personal marketplace file, confirm that the selected marketplace is + local before telling the user to reinstall from it. +- If the selected marketplace is not local, stop and help the user resolve that mismatch rather + than pretending the normal local reinstall flow applies. +- If the plugin source is not already the source referenced by the chosen marketplace entry, stop + and fix that first. This update flow does not rewrite marketplace entries. + +## After Reinstall + +After reinstalling, prompt the user to start a new thread for testing. That is the safe boundary for +picking up the updated plugin and its MCP tools. diff --git a/.agents/skills/plugin-creator/references/plugin-json-spec.md b/.agents/skills/plugin-creator/references/plugin-json-spec.md index 0fd444e6..40d3ae6b 100644 --- a/.agents/skills/plugin-creator/references/plugin-json-spec.md +++ b/.agents/skills/plugin-creator/references/plugin-json-spec.md @@ -36,6 +36,7 @@ "brandColor": "#3B82F6", "composerIcon": "./assets/icon.png", "logo": "./assets/logo.png", + "logoDark": "./assets/logo-dark.png", "screenshots": [ "./assets/screenshot1.png", "./assets/screenshot2.png", @@ -62,10 +63,31 @@ - `keywords` (`array` of `string`): Search/discovery tags. - `skills` (`string`): Relative path to skill directories/files. - `hooks` (`string`): Hook config path. -- `mcpServers` (`string`): MCP config path. +- `mcpServers` (`string` or `object`): MCP config path, or an object whose keys are MCP server names and whose values are MCP server config objects. - `apps` (`string`): App manifest path for plugin integrations. - `interface` (`object`): Interface/UX metadata block for plugin presentation. +`mcpServers` may be declared as a companion file path: + +```json +{ + "mcpServers": "./.mcp.json" +} +``` + +Or as an object directly in `plugin.json`: + +```json +{ + "mcpServers": { + "counter": { + "type": "http", + "url": "https://sample.example/counter/mcp" + } + } +} +``` + ### `interface` fields - `displayName` (`string`): User-facing title shown for the plugin. @@ -84,6 +106,7 @@ - `brandColor` (`string`): Theme color for the plugin card. - `composerIcon` (`string`): Path to icon asset. - `logo` (`string`): Path to logo asset. +- `logoDark` (`string`): Optional path to the logo asset used in dark mode. - `screenshots` (`array` of `string`): List of screenshot asset paths. - Screenshot entries must be PNG filenames and stored under `./assets/`. - Keep file paths relative to plugin root. @@ -91,7 +114,7 @@ ### Path conventions and defaults - Path values should be relative and begin with `./`. -- `skills`, `hooks`, and `mcpServers` are supplemented on top of default component discovery; they do not replace defaults. +- `skills`, `hooks`, and string-valued `mcpServers` are supplemented on top of default component discovery; they do not replace defaults. - Custom path values must follow the plugin root convention and naming/namespacing rules. - This repo’s scaffold writes `.codex-plugin/plugin.json`; treat that as the manifest location this skill generates. @@ -147,7 +170,8 @@ personal marketplace unless the caller explicitly requests a repo-local destinat - Personal plugin in `~/.agents/plugins/marketplace.json`: `./plugins/` - Repo/team plugin: `./plugins/` - The same relative path convention is used for both personal and repo/team marketplaces. - - Example: with `~/.agents/plugins/marketplace.json`, `./plugins/` resolves to `~/plugins/`. + - Example: with `~/.agents/plugins/marketplace.json`, `./plugins/` resolves to + `~/plugins/`. - `policy` (`object`): Marketplace policy block. Always include it. - `installation` (`string`): Availability policy. - Allowed values: `NOT_AVAILABLE`, `AVAILABLE`, `INSTALLED_BY_DEFAULT` @@ -168,6 +192,8 @@ personal marketplace unless the caller explicitly requests a repo-local destinat - Replace an existing entry for the same plugin only when overwrite is intentional. - Default new plugin creation to the personal marketplace. - Use a repo/team marketplace only when the user specifically requests that destination. +- Only override the marketplace `name` when the default `personal` name is already taken or + installed and you need to seed a different new marketplace file. - Choose marketplace location to match the selected destination: - Personal plugin: `~/.agents/plugins/marketplace.json` - Repo/team plugin: `/.agents/plugins/marketplace.json` @@ -181,10 +207,11 @@ personal marketplace unless the caller explicitly requests a repo-local destinat - `version` must use strict semver. - `websiteURL`, `privacyPolicyURL`, and `termsOfServiceURL` must be absolute `https://` URLs when present. -- `composerIcon`, `logo`, and `screenshots` must point to real files inside the plugin archive when +- `composerIcon`, `logo`, `logoDark`, and `screenshots` must point to real files inside the plugin archive when present. -- `apps` and `mcpServers` should appear in `plugin.json` only when `.app.json` and `.mcp.json` - actually exist. +- `apps` should appear in `plugin.json` only when `.app.json` actually exists. +- `mcpServers` may point to `.mcp.json` or contain the MCP server object directly in + `plugin.json`. - Validation rejects unsupported manifest fields such as `hooks`, so the scaffold keeps them out of generated manifests. - Run `scripts/validate_plugin.py ` before handing back a generated plugin. It adds one diff --git a/.agents/skills/plugin-creator/scripts/create_basic_plugin.py b/.agents/skills/plugin-creator/scripts/create_basic_plugin.py index fb882ce9..78b9fca8 100755 --- a/.agents/skills/plugin-creator/scripts/create_basic_plugin.py +++ b/.agents/skills/plugin-creator/scripts/create_basic_plugin.py @@ -11,15 +11,14 @@ MAX_PLUGIN_NAME_LENGTH = 64 -DEFAULT_PLUGIN_PARENT = Path.home() / "plugins" -DEFAULT_MARKETPLACE_PATH = Path.home() / ".agents" / "plugins" / "marketplace.json" DEFAULT_INSTALL_POLICY = "AVAILABLE" DEFAULT_AUTH_POLICY = "ON_INSTALL" DEFAULT_CATEGORY = "Productivity" DEFAULT_MARKETPLACE_NAME = "personal" -DEFAULT_MARKETPLACE_DISPLAY_NAME = "Personal" VALID_INSTALL_POLICIES = {"NOT_AVAILABLE", "AVAILABLE", "INSTALLED_BY_DEFAULT"} VALID_AUTH_POLICIES = {"ON_INSTALL", "ON_USE"} +DEFAULT_PLUGIN_PARENT = Path.home() / "plugins" +DEFAULT_MARKETPLACE_PATH = Path.home() / ".agents" / "plugins" / "marketplace.json" def normalize_plugin_name(plugin_name: str) -> str: @@ -41,8 +40,17 @@ def validate_plugin_name(plugin_name: str) -> None: ) +def validate_marketplace_name(marketplace_name: str) -> None: + if not marketplace_name: + raise ValueError("Marketplace name must include at least one letter or digit.") + if re.fullmatch(r"[A-Za-z0-9_-]+", marketplace_name) is None: + raise ValueError( + "Marketplace name may only contain ASCII letters, digits, `_`, and `-`." + ) + + def display_name_from_plugin_name(plugin_name: str) -> str: - return " ".join(part.capitalize() for part in plugin_name.split("-")) + return " ".join(part.capitalize() for part in re.split(r"[-_]+", plugin_name)) def build_plugin_json(plugin_name: str, *, with_mcp: bool, with_apps: bool) -> dict[str, Any]: @@ -97,11 +105,11 @@ def load_json(path: Path) -> dict[str, Any]: return json.load(handle) -def build_default_marketplace() -> dict[str, Any]: +def build_default_marketplace(marketplace_name: str) -> dict[str, Any]: return { - "name": DEFAULT_MARKETPLACE_NAME, + "name": marketplace_name, "interface": { - "displayName": DEFAULT_MARKETPLACE_DISPLAY_NAME, + "displayName": display_name_from_plugin_name(marketplace_name), }, "plugins": [], } @@ -115,6 +123,7 @@ def validate_marketplace_interface(payload: dict[str, Any]) -> None: def update_marketplace_json( marketplace_path: Path, + marketplace_name: str | None, plugin_name: str, install_policy: str, auth_policy: str, @@ -124,13 +133,24 @@ def update_marketplace_json( if marketplace_path.exists(): payload = load_json(marketplace_path) else: - payload = build_default_marketplace() + payload = build_default_marketplace(marketplace_name or DEFAULT_MARKETPLACE_NAME) if not isinstance(payload, dict): raise ValueError(f"{marketplace_path} must contain a JSON object.") validate_marketplace_interface(payload) + existing_marketplace_name = payload.get("name") + if marketplace_name is not None: + if not isinstance(existing_marketplace_name, str) or not existing_marketplace_name.strip(): + raise ValueError(f"{marketplace_path} must contain a non-empty string 'name'.") + if existing_marketplace_name != marketplace_name: + raise ValueError( + f"{marketplace_path} already uses marketplace name " + f"'{existing_marketplace_name}'. Create a new marketplace file to use " + f"'{marketplace_name}' instead." + ) + plugins = payload.setdefault("plugins", []) if not isinstance(plugins, list): raise ValueError(f"{marketplace_path} field 'plugins' must be an array.") @@ -206,6 +226,13 @@ def parse_args() -> argparse.Namespace: "Pass a repo-rooted marketplace path only when a repo/team plugin is intended." ), ) + parser.add_argument( + "--marketplace-name", + help=( + "Marketplace name to seed into a new marketplace.json. Use this only when the default " + "'personal' marketplace name is already taken and you need a different new marketplace." + ), + ) parser.add_argument( "--install-policy", default=DEFAULT_INSTALL_POLICY, @@ -234,6 +261,10 @@ def main() -> None: if plugin_name != raw_plugin_name: print(f"Note: Normalized plugin name from '{raw_plugin_name}' to '{plugin_name}'.") validate_plugin_name(plugin_name) + marketplace_name = None + if args.marketplace_name is not None: + marketplace_name = args.marketplace_name.strip() + validate_marketplace_name(marketplace_name) plugin_root = (Path(args.path).expanduser().resolve() / plugin_name) plugin_root.mkdir(parents=True, exist_ok=True) @@ -275,6 +306,7 @@ def main() -> None: marketplace_path = Path(args.marketplace_path).expanduser().resolve() update_marketplace_json( marketplace_path, + marketplace_name, plugin_name, args.install_policy, args.auth_policy, diff --git a/.agents/skills/plugin-creator/scripts/read_marketplace_name.py b/.agents/skills/plugin-creator/scripts/read_marketplace_name.py new file mode 100644 index 00000000..597e9f7a --- /dev/null +++ b/.agents/skills/plugin-creator/scripts/read_marketplace_name.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Print the top-level marketplace name from any marketplace.json file.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + + +def default_marketplace_path() -> Path: + return Path.home() / ".agents" / "plugins" / "marketplace.json" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Print the top-level marketplace name from marketplace.json. Defaults to the personal " + "marketplace path under the current home directory." + ) + ) + parser.add_argument( + "--marketplace-path", + default=str(default_marketplace_path()), + help="Path to marketplace.json", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + marketplace_path = Path(args.marketplace_path).expanduser().resolve() + payload = json.loads(marketplace_path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise ValueError(f"{marketplace_path} must contain a JSON object.") + name = payload.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError(f"{marketplace_path} must contain a non-empty string 'name'.") + print(name.strip()) + + +if __name__ == "__main__": + try: + main() + except Exception as err: # noqa: BLE001 - CLI should surface a single clear message. + print(str(err), file=sys.stderr) + raise SystemExit(1) from err diff --git a/.agents/skills/plugin-creator/scripts/update_plugin_cachebuster.py b/.agents/skills/plugin-creator/scripts/update_plugin_cachebuster.py new file mode 100644 index 00000000..82d19e5d --- /dev/null +++ b/.agents/skills/plugin-creator/scripts/update_plugin_cachebuster.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Rewrite a local plugin version to a single Codex cachebuster suffix.""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from datetime import datetime, timezone +from pathlib import Path + + +CACHEBUSTER_PREFIX = "codex" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Rewrite a local plugin's version so it preserves everything before '+' and uses " + "a single +codex. suffix." + ) + ) + parser.add_argument("plugin_path", help="Path to the plugin root directory") + parser.add_argument( + "--cachebuster", + help="Optional cachebuster token to embed in the plugin version", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + plugin_root = Path(args.plugin_path).expanduser().resolve() + manifest_path = plugin_root / ".codex-plugin" / "plugin.json" + manifest = load_manifest(manifest_path) + + version = manifest.get("version") + if not isinstance(version, str) or not version.strip(): + raise ValueError(f"{manifest_path} must contain a non-empty string 'version'.") + cachebuster = sanitize_cachebuster(args.cachebuster or default_cachebuster()) + next_version = with_cachebuster(version, cachebuster) + manifest["version"] = next_version + manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + + print(f"Updated plugin version: {version} -> {next_version}") + + +def load_manifest(manifest_path: Path) -> dict[str, object]: + if not manifest_path.is_file(): + raise FileNotFoundError(f"missing manifest: {manifest_path}") + payload = json.loads(manifest_path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise ValueError(f"{manifest_path} must contain a JSON object.") + return payload +def sanitize_cachebuster(value: str) -> str: + sanitized = re.sub(r"[^a-z0-9-]+", "-", value.strip().lower()) + sanitized = re.sub(r"-{2,}", "-", sanitized).strip("-") + if not sanitized: + raise ValueError("Cachebuster must contain at least one letter or digit.") + return sanitized + + +def default_cachebuster() -> str: + return datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S") + + +def with_cachebuster(version: str, cachebuster: str) -> str: + version_prefix = version.split("+", 1)[0] + return f"{version_prefix}+{CACHEBUSTER_PREFIX}.{cachebuster}" + + +if __name__ == "__main__": + try: + main() + except Exception as err: # noqa: BLE001 - CLI should surface a single clear message. + print(str(err), file=sys.stderr) + raise SystemExit(1) from err diff --git a/.agents/skills/plugin-creator/scripts/validate_plugin.py b/.agents/skills/plugin-creator/scripts/validate_plugin.py index 64518064..88fae0fd 100644 --- a/.agents/skills/plugin-creator/scripts/validate_plugin.py +++ b/.agents/skills/plugin-creator/scripts/validate_plugin.py @@ -126,18 +126,13 @@ def validate_manifest_shape( validate_optional_contract_path(manifest, "skills", "skills", errors) validate_optional_contract_path(manifest, "apps", ".app.json", errors) - validate_optional_contract_path(manifest, "mcpServers", ".mcp.json", errors) + validate_manifest_mcp_servers(plugin_root, manifest, errors) if manifest.get("apps") is not None: validate_app_manifest( plugin_root / ".app.json", errors, ) - if manifest.get("mcpServers") is not None: - validate_mcp_manifest( - plugin_root / ".mcp.json", - errors, - ) validate_skill_manifests(plugin_root, errors) interface = require_object(manifest, "interface", errors) @@ -158,6 +153,7 @@ def validate_manifest_shape( "brandColor", "composerIcon", "logo", + "logoDark", "screenshots", "defaultPrompt", "default_prompt", @@ -189,7 +185,7 @@ def validate_manifest_shape( not isinstance(brand_color, str) or HEX_COLOR_RE.fullmatch(brand_color) is None ): errors.append("plugin.json field `interface.brandColor` must use `#RRGGBB`") - for field in ("composerIcon", "logo"): + for field in ("composerIcon", "logo", "logoDark"): validate_optional_asset_path(plugin_root, plugin_root, interface, field, errors) screenshots = interface.get("screenshots", []) if not isinstance(screenshots, list): @@ -286,6 +282,32 @@ def validate_optional_contract_path( errors.append(f"plugin.json field `{key}` must resolve to `{expected}`") +def validate_manifest_mcp_servers( + plugin_root: Path, + manifest: dict[str, Any], + errors: list[str], +) -> None: + value = manifest.get("mcpServers") + if value is None: + return + if isinstance(value, str): + validate_optional_contract_path(manifest, "mcpServers", ".mcp.json", errors) + validate_mcp_manifest( + plugin_root / ".mcp.json", + errors, + ) + return + if isinstance(value, dict): + validate_mcp_server_entries( + value, + "plugin.json field `mcpServers`", + "plugin.json field `mcpServers`", + errors, + ) + return + errors.append("plugin.json field `mcpServers` must be a string path or object") + + def normalize_contract_path(raw_path: str) -> str | None: path = Path(raw_path) if path.is_absolute(): @@ -307,10 +329,17 @@ def validate_app_manifest(path: Path, errors: list[str]) -> None: if not isinstance(value, dict): errors.append(f"`.app.json` app `{key}` must be an object") continue - reject_companion_unknown_fields(value, {"id"}, f"`.app.json` app `{key}`", errors) + reject_companion_unknown_fields( + value, {"id", "category"}, f"`.app.json` app `{key}`", errors + ) app_id = value.get("id") if not isinstance(app_id, str) or not app_id.strip(): errors.append(f"`.app.json` app `{key}` field `id` must be a non-empty string") + category = value.get("category") + if category is not None and (not isinstance(category, str) or not category.strip()): + errors.append( + f"`.app.json` app `{key}` field `category` must be a non-empty string" + ) def validate_mcp_manifest(path: Path, errors: list[str]) -> None: @@ -319,14 +348,28 @@ def validate_mcp_manifest(path: Path, errors: list[str]) -> None: return reject_companion_unknown_fields(payload, {"mcpServers"}, "`.mcp.json`", errors) servers = payload.get("mcpServers") + validate_mcp_server_entries( + servers, + "`.mcp.json`", + "`.mcp.json` field `mcpServers`", + errors, + ) + + +def validate_mcp_server_entries( + servers: Any, + source_label: str, + field_label: str, + errors: list[str], +) -> None: if not isinstance(servers, dict): - errors.append("`.mcp.json` field `mcpServers` must be an object") + errors.append(f"{field_label} must be an object") return for key, value in servers.items(): if not isinstance(key, str) or not key.strip(): - errors.append("`.mcp.json` server names must be non-empty strings") + errors.append(f"{source_label} server names must be non-empty strings") if not isinstance(value, dict): - errors.append(f"`.mcp.json` server `{key}` must be an object") + errors.append(f"{source_label} server `{key}` must be an object") def load_companion_json_object( diff --git a/agent/skills/plugin-creator/SKILL.md b/agent/skills/plugin-creator/SKILL.md new file mode 100644 index 00000000..9946fa6b --- /dev/null +++ b/agent/skills/plugin-creator/SKILL.md @@ -0,0 +1,241 @@ +--- +description: "Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow." +--- +# Plugin Creator + +## Quick Start + +1. Run the scaffold script: + +```bash +# Plugin names are normalized to lower-case hyphen-case and must be <= 64 chars. +# The generated folder and plugin.json name are always the same. +# Run from the skill root (the directory containing this `SKILL.md`). +# By default creates in `~/plugins/`. +python3 scripts/create_basic_plugin.py +``` + +2. Edit `/.codex-plugin/plugin.json` when the request gives specific metadata. + The scaffold starts with valid defaults and must not contain `[TODO: ...]` placeholders. + +3. Generate or update the personal marketplace entry when the plugin should appear in Codex UI ordering: + +```bash +# Personal marketplace entries default to `~/.agents/plugins/marketplace.json`. +python3 scripts/create_basic_plugin.py my-plugin --with-marketplace +``` + +Only specify `--marketplace-name ` when the default `personal` marketplace name is already +taken or installed and you need to seed a different new marketplace file: + +```bash +python3 scripts/create_basic_plugin.py my-plugin \ + --with-marketplace \ + --marketplace-name team-local +``` + +Only use a repo/team marketplace when the user specifically asks for that destination: + +```bash +python3 scripts/create_basic_plugin.py my-plugin \ + --path /plugins \ + --marketplace-path /.agents/plugins/marketplace.json \ + --with-marketplace +``` + +When the user specifies a marketplace path, make sure that marketplace is actually installed before +telling the user to reinstall from it. The default personal marketplace file at +`~/.agents/plugins/marketplace.json` is discovered implicitly, but other marketplace paths are not. +On Windows, use the equivalent path under the user profile. + +4. Generate/adjust optional companion folders as needed: + +```bash +python3 scripts/create_basic_plugin.py my-plugin \ + --path \ + --marketplace-path \ + --with-skills --with-hooks --with-scripts --with-assets --with-mcp --with-apps --with-marketplace +``` + +`` is the directory where the plugin folder `` will be +created (for example `~/plugins`). + +5. Before handing back a generated plugin, run: + +```bash +python3 scripts/validate_plugin.py +``` + +For updates to an existing local plugin during development, keep the scaffold flow as-is and use the +reference instead of hand-editing marketplace files: + +```bash +python3 scripts/update_plugin_cachebuster.py +``` + +Prefer the helper default cachebuster unless the user explicitly asks for a specific override. +See `references/installing-and-updating.md` for the expected cachebuster and reinstall flow while iterating on an existing local plugin. + +## What this skill creates + +- Default marketplace-backed scaffolds use the personal marketplace file at + `~/.agents/plugins/marketplace.json`, with plugins generally being stored in + `~/plugins//`. +- Creates plugin root at `///`. +- Always creates `///.codex-plugin/plugin.json`. +- Fills the manifest with the validated schema shape that the ingestion path accepts. +- Creates or updates `~/.agents/plugins/marketplace.json` when `--with-marketplace` is set. + - If the marketplace file does not exist yet, seed a personal marketplace root before adding the first plugin entry. +- `` is normalized using skill-creator naming rules: + - `My Plugin` → `my-plugin` + - `My--Plugin` → `my-plugin` + - underscores, spaces, and punctuation are converted to `-` + - result is lower-case hyphen-delimited with consecutive hyphens collapsed +- Supports optional creation of: + - `skills/` + - `hooks/` + - `scripts/` + - `assets/` + - `.mcp.json` + - `.app.json` + +## Marketplace workflow + +- Personal-marketplace creation defaults to `~/.agents/plugins/marketplace.json`. Here, + "personal marketplace" means the marketplace whose file is at that path. +- Repo/team marketplace creation is opt-in through both `--path` and `--marketplace-path`, only + when the user specifically requests it. +- `--marketplace-name` is an exception path. Use it only when the default `personal` marketplace + name is already taken and you need to seed a different new marketplace file. +- Do not use `--marketplace-name` to rename an existing marketplace file in place. If the file + already exists, its top-level `name` must already match. +- If the user specifies a different marketplace path, treat that marketplace as needing explicit installation via `codex plugin marketplace add`. +- Prefer `scripts/read_marketplace_name.py` when you need the marketplace name from any + `marketplace.json` file. With no argument it reads the default personal marketplace; with an + explicit path it works for repo/team marketplaces too. +- In either location, the generated source path remains `./plugins/`. +- Marketplace root metadata supports top-level `name` plus optional `interface.displayName`. +- Treat plugin order in `plugins[]` as render order in Codex. Append new entries unless a user explicitly asks to reorder the list. +- `displayName` belongs inside the marketplace `interface` object, not individual `plugins[]` entries. +- Each generated marketplace entry must include all of: + - `policy.installation` + - `policy.authentication` + - `category` +- Default new entries to: + - `policy.installation: "AVAILABLE"` + - `policy.authentication: "ON_INSTALL"` +- Override defaults only when the user explicitly specifies another allowed value. +- Allowed `policy.installation` values: + - `NOT_AVAILABLE` + - `AVAILABLE` + - `INSTALLED_BY_DEFAULT` +- Allowed `policy.authentication` values: + - `ON_INSTALL` + - `ON_USE` +- Treat `policy.products` as an override. Omit it unless the user explicitly requests product gating. +- The generated plugin entry shape is: + +```json +{ + "name": "plugin-name", + "source": { + "source": "local", + "path": "./plugins/plugin-name" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" +} +``` + +- Use `--force` only when intentionally replacing an existing marketplace entry for the same plugin name. +- If the target marketplace file does not exist yet, create it with top-level `"name"`, an `"interface"` object containing `"displayName"`, and a `plugins` array, then add the new entry. + +- For a brand-new marketplace file, the root object should look like: + +```json +{ + "name": "personal", + "interface": { + "displayName": "Personal" + }, + "plugins": [ + { + "name": "plugin-name", + "source": { + "source": "local", + "path": "./plugins/plugin-name" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + } + ] +} +``` + +## Required behavior + +- Outer folder name and `plugin.json` `"name"` are always the same normalized plugin name. +- Do not remove required structure; keep `.codex-plugin/plugin.json` present. +- Do not leave `[TODO: ...]` placeholders in plugin manifests. +- Keep `apps` and `mcpServers` out of `plugin.json` unless their companion files are actually created. +- Omit unsupported plugin manifest fields that validation rejects, including `hooks`. +- If creating files inside an existing plugin path, use `--force` only when overwrite is intentional. +- Preserve any existing marketplace `interface.displayName`. +- When generating marketplace entries, always write `policy.installation`, `policy.authentication`, and `category` even if their values are defaults. +- Add `policy.products` only when the user explicitly asks for that override. +- Keep marketplace `source.path` relative to the selected marketplace root as `./plugins/`. +- Only use `--marketplace-name` when creating a new marketplace file whose name should not be + `personal` because that name is already taken or installed elsewhere. +- If Codex would need approval to write the marketplace file, ask for that approval before + proceeding. If the user prefers to run the write themselves, provide the exact scaffold command + and then continue from validation or subsequent plugin edits instead of leaving the workflow + vague. +- For updates to an existing local plugin during development, do not hand-edit marketplace config + or `marketplace.json`. Use the update flow documented in + `references/installing-and-updating.md` and `scripts/update_plugin_cachebuster.py`. +- Do not tell the user to run `codex plugin marketplace add` for the default personal-marketplace + flow. That command is for explicit non-default marketplace configuration, not for the standard + `~/.agents/plugins/marketplace.json` path. +- If the user provided a non-default `--marketplace-path`, make sure that marketplace is installed + before giving reinstall instructions. Use `codex plugin marketplace add ` + when that explicit marketplace has not been configured yet. +- When the workflow created or updated a marketplace-backed plugin, end the final user-facing + response with a short Codex app handoff. Say `To view this in the Codex app:` and write + `View ` and `Share ` as Markdown links, not raw + URLs or code spans. +- The View deeplink uses `codex://plugins/?marketplacePath=`. + The Share deeplink uses the same URL with `&mode=share`. +- Replace the placeholders with the real normalized plugin name and absolute `marketplace.json` + path from the scaffolded plugin. URL-encode the path segment and query value when needed. +- Do not add `pluginName` or `hostId` query parameters to these deeplinks. Codex derives both after + the user clicks the link. +- Do not emit the `View ` or `Share ` links when no marketplace entry was + created or updated. + +## Reference to exact spec sample + +For the exact canonical sample JSON for both plugin manifests and marketplace entries, use: + +- `references/plugin-json-spec.md` +- `references/installing-and-updating.md` for update/reinstall guidance while + iterating on an existing local plugin, plus the new-thread pickup behavior after reinstall + +## Validation + +After editing `SKILL.md`, run: + +```bash +python3 ../skill-creator/scripts/quick_validate.py . +``` + +Before handing back a generated plugin, run: + +```bash +python3 scripts/validate_plugin.py +``` diff --git a/agent/skills/plugin-creator/agents/openai.yaml b/agent/skills/plugin-creator/agents/openai.yaml new file mode 100644 index 00000000..19a9a6fc --- /dev/null +++ b/agent/skills/plugin-creator/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "Plugin Creator" + short_description: "Scaffold plugins and marketplace entries" + default_prompt: "Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back." + icon_small: "./assets/plugin-creator-small.svg" + icon_large: "./assets/plugin-creator.png" diff --git a/agent/skills/plugin-creator/assets/plugin-creator-small.svg b/agent/skills/plugin-creator/assets/plugin-creator-small.svg new file mode 100644 index 00000000..c6e4f67c --- /dev/null +++ b/agent/skills/plugin-creator/assets/plugin-creator-small.svg @@ -0,0 +1,3 @@ + + + diff --git a/agent/skills/plugin-creator/assets/plugin-creator.png b/agent/skills/plugin-creator/assets/plugin-creator.png new file mode 100644 index 00000000..4f3d6d82 Binary files /dev/null and b/agent/skills/plugin-creator/assets/plugin-creator.png differ diff --git a/agent/skills/plugin-creator/references/installing-and-updating.md b/agent/skills/plugin-creator/references/installing-and-updating.md new file mode 100644 index 00000000..28b3b889 --- /dev/null +++ b/agent/skills/plugin-creator/references/installing-and-updating.md @@ -0,0 +1,143 @@ +# Updating Existing Local Plugins + +Use this reference when a plugin already exists and the request is about updating the plugin during +local development. + +All scripts here are specified relative to the skill root. Update the path for running the scripts +depending on your current working directory. + +## When To Use This Flow + +Use this flow when all of the following are true: + +- the plugin already exists locally +- the marketplace entry already points at the plugin source you are editing +- the user wants Codex to see the updated plugin without manually editing marketplace files + +If the user still needs the initial plugin entry or marketplace structure created, use the scaffold +flow first and only then switch to this reinstall flow. + +## Update Loop + +1. Update the plugin manifest to a single Codex cachebuster suffix: + +```bash +python3 scripts/update_plugin_cachebuster.py \ + +``` + +Prefer the default helper behavior here. If you omit `--cachebuster`, the helper uses a UTC +timestamp down to seconds, which is the recommended path for routine local iteration. + +Only use a manual cachebuster override when the user explicitly asks for one or when a workflow +outside Codex depends on a specific token: + +```bash +python3 scripts/update_plugin_cachebuster.py \ + \ + --cachebuster local-20260519-184516 +``` + +2. For the default scaffolded flow, read the marketplace name from the personal marketplace file: + +```bash +python3 scripts/read_marketplace_name.py +``` + +Here, "personal marketplace" means the marketplace whose file is at +`~/.agents/plugins/marketplace.json`. On Windows, use the equivalent path under the user profile. +The helper uses Python's home-directory resolution and prints the marketplace name to use when +constructing the install command. + +To read the name from a different marketplace file, pass the path directly: + +```bash +python3 scripts/read_marketplace_name.py --marketplace-path +``` + +3. Reinstall from that marketplace name: + +```bash +codex plugin add @ +``` + +The default personal marketplace is discovered implicitly from +`~/.agents/plugins/marketplace.json`. You do not need `codex plugin marketplace add` for that +path, and `codex plugin marketplace list` is not the right check for whether that default +marketplace exists. + +4. If the plugin is not using the personal marketplace file, check which configured local + marketplace is actually surfacing that plugin: + +```bash +codex plugin list +``` + +If the plugin is not in the personal marketplace file, confirm which marketplace entry points at +the plugin source you are editing and make sure that marketplace is still local. If it is a +different local marketplace, reinstall from that marketplace name instead of forcing the personal +marketplace flow. If it is not local, stop and help the user resolve the mismatch before +continuing. + +5. If the plugin lives in a different confirmed local marketplace, substitute that marketplace + name: + +```bash +codex plugin add @ +``` + +6. Prompt the user to use a new thread to try the updated plugin, so that Codex picks up new skills + and tools. + +## Cachebuster Policy + +- Preserve the existing version prefix and replace only the suffix. +- Treat the preserved prefix as everything before `+`. +- Use the format: + +```text ++codex. +``` + +Examples: + +- `0.1.0` → `0.1.0+codex.local-20260519-184516` +- `0.1.0+codex.old-token` → `0.1.0+codex.local-20260519-184516` +- `1.2.3-beta.1+codex.prev` → `1.2.3-beta.1+codex.local-20260519-184516` +- `dev-build+other-tag` → `dev-build+codex.local-20260519-184516` + +Replace the existing Codex cachebuster instead of appending another one. Do not keep incrementing +numeric version components just to trigger reinstall behavior. + +## Marketplace Rules + +- Marketplace manipulation should happen through commands, not by hand-editing `marketplace.json` + or `config.toml` during this update/reinstall flow. +- Prefer the personal marketplace file for the default scaffolded flow. +- Read the personal marketplace name with + `python3 scripts/read_marketplace_name.py` and use the printed value when constructing + `codex plugin add @`. +- For non-default marketplace files, use + `python3 scripts/read_marketplace_name.py --marketplace-path ` to read + the name before constructing reinstall commands. +- Do not tell the user to run `codex plugin marketplace add` for the default personal-marketplace + flow. That marketplace is discovered implicitly by Codex. +- If the user specified a different marketplace path, make sure that marketplace is installed + before giving install or reinstall instructions. Non-default marketplace paths are not + discovered implicitly. +- Use `codex plugin list` when the plugin lives in a different configured marketplace and you need + to confirm which marketplace is surfacing that plugin. +- If a non-default local marketplace has not been configured yet, install it with + `codex plugin marketplace add ` before telling the user to run + `codex plugin add @`. +- If the plugin is not in the personal marketplace file, confirm that the selected marketplace is + local before telling the user to reinstall from it. +- If the selected marketplace is not local, stop and help the user resolve that mismatch rather + than pretending the normal local reinstall flow applies. +- If the plugin source is not already the source referenced by the chosen marketplace entry, stop + and fix that first. This update flow does not rewrite marketplace entries. + +## After Reinstall + +After reinstalling, prompt the user to start a new thread for testing. That is the safe boundary for +picking up the updated plugin and its MCP tools. diff --git a/agent/skills/plugin-creator/references/plugin-json-spec.md b/agent/skills/plugin-creator/references/plugin-json-spec.md new file mode 100644 index 00000000..40d3ae6b --- /dev/null +++ b/agent/skills/plugin-creator/references/plugin-json-spec.md @@ -0,0 +1,218 @@ +# Plugin JSON sample spec + +```json +{ + "name": "plugin-name", + "version": "1.2.0", + "description": "Brief plugin description", + "author": { + "name": "Author Name", + "email": "author@example.com", + "url": "https://github.com/author" + }, + "homepage": "https://docs.example.com/plugin", + "repository": "https://github.com/author/plugin", + "license": "MIT", + "keywords": ["keyword1", "keyword2"], + "skills": "./skills/", + "hooks": "./hooks.json", + "mcpServers": "./.mcp.json", + "apps": "./.app.json", + "interface": { + "displayName": "Plugin Display Name", + "shortDescription": "Short description for subtitle", + "longDescription": "Long description for details page", + "developerName": "OpenAI", + "category": "Productivity", + "capabilities": ["Interactive", "Write"], + "websiteURL": "https://openai.com/", + "privacyPolicyURL": "https://openai.com/policies/row-privacy-policy/", + "termsOfServiceURL": "https://openai.com/policies/row-terms-of-use/", + "defaultPrompt": [ + "Summarize my inbox and draft replies for me.", + "Find open bugs and turn them into Linear tickets.", + "Review today's meetings and flag scheduling gaps." + ], + "brandColor": "#3B82F6", + "composerIcon": "./assets/icon.png", + "logo": "./assets/logo.png", + "logoDark": "./assets/logo-dark.png", + "screenshots": [ + "./assets/screenshot1.png", + "./assets/screenshot2.png", + "./assets/screenshot3.png" + ] + } +} +``` + +## Field guide + +### Top-level fields + +- `name` (`string`): Plugin identifier (kebab-case, no spaces). Required if `plugin.json` is provided and used as manifest name and component namespace. +- `version` (`string`): Plugin semantic version. +- `description` (`string`): Short purpose summary. +- `author` (`object`): Publisher identity. + - `name` (`string`): Author or team name. + - `email` (`string`): Contact email. + - `url` (`string`): Author/team homepage or profile URL. +- `homepage` (`string`): Documentation URL for plugin usage. +- `repository` (`string`): Source code URL. +- `license` (`string`): License identifier (for example `MIT`, `Apache-2.0`). +- `keywords` (`array` of `string`): Search/discovery tags. +- `skills` (`string`): Relative path to skill directories/files. +- `hooks` (`string`): Hook config path. +- `mcpServers` (`string` or `object`): MCP config path, or an object whose keys are MCP server names and whose values are MCP server config objects. +- `apps` (`string`): App manifest path for plugin integrations. +- `interface` (`object`): Interface/UX metadata block for plugin presentation. + +`mcpServers` may be declared as a companion file path: + +```json +{ + "mcpServers": "./.mcp.json" +} +``` + +Or as an object directly in `plugin.json`: + +```json +{ + "mcpServers": { + "counter": { + "type": "http", + "url": "https://sample.example/counter/mcp" + } + } +} +``` + +### `interface` fields + +- `displayName` (`string`): User-facing title shown for the plugin. +- `shortDescription` (`string`): Brief subtitle used in compact views. +- `longDescription` (`string`): Longer description used on details screens. +- `developerName` (`string`): Human-readable publisher name. +- `category` (`string`): Plugin category bucket. +- `capabilities` (`array` of `string`): Capability list from implementation. +- `websiteURL` (`string`): Public website for the plugin. +- `privacyPolicyURL` (`string`): Privacy policy URL. +- `termsOfServiceURL` (`string`): Terms of service URL. +- `defaultPrompt` (`array` of `string`): Starter prompts shown in composer/UX context. + - Include at most 3 strings. Entries after the first 3 are ignored and will not be included. + - Each string is capped at 128 characters. Longer entries are truncated. + - Prefer short starter prompts around 50 characters so they scan well in the UI. +- `brandColor` (`string`): Theme color for the plugin card. +- `composerIcon` (`string`): Path to icon asset. +- `logo` (`string`): Path to logo asset. +- `logoDark` (`string`): Optional path to the logo asset used in dark mode. +- `screenshots` (`array` of `string`): List of screenshot asset paths. + - Screenshot entries must be PNG filenames and stored under `./assets/`. + - Keep file paths relative to plugin root. + +### Path conventions and defaults + +- Path values should be relative and begin with `./`. +- `skills`, `hooks`, and string-valued `mcpServers` are supplemented on top of default component discovery; they do not replace defaults. +- Custom path values must follow the plugin root convention and naming/namespacing rules. +- This repo’s scaffold writes `.codex-plugin/plugin.json`; treat that as the manifest location this skill generates. + +# Marketplace JSON sample spec + +`marketplace.json` depends on where the plugin should live. New plugin creation defaults to the +personal marketplace unless the caller explicitly requests a repo-local destination: + +- Personal plugin: `~/.agents/plugins/marketplace.json` +- Repo/team plugin: `/.agents/plugins/marketplace.json` + +```json +{ + "name": "openai-curated", + "interface": { + "displayName": "ChatGPT Official" + }, + "plugins": [ + { + "name": "linear", + "source": { + "source": "local", + "path": "./plugins/linear" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + } + ] +} +``` + +## Marketplace field guide + +### Top-level fields + +- `name` (`string`): Marketplace identifier or catalog name. +- `interface` (`object`, optional): Marketplace presentation metadata. +- `plugins` (`array`): Ordered plugin entries. This order determines how Codex renders plugins. + +### `interface` fields + +- `displayName` (`string`, optional): User-facing marketplace title. + +### Plugin entry fields + +- `name` (`string`): Plugin identifier. Match the plugin folder name and `plugin.json` `name`. +- `source` (`object`): Plugin source descriptor. + - `source` (`string`): Use `local` for this repo workflow. + - `path` (`string`): Relative plugin path based on the marketplace root. + - Personal plugin in `~/.agents/plugins/marketplace.json`: `./plugins/` + - Repo/team plugin: `./plugins/` + - The same relative path convention is used for both personal and repo/team marketplaces. + - Example: with `~/.agents/plugins/marketplace.json`, `./plugins/` resolves to + `~/plugins/`. +- `policy` (`object`): Marketplace policy block. Always include it. + - `installation` (`string`): Availability policy. + - Allowed values: `NOT_AVAILABLE`, `AVAILABLE`, `INSTALLED_BY_DEFAULT` + - Default for new entries: `AVAILABLE` + - `authentication` (`string`): Authentication timing policy. + - Allowed values: `ON_INSTALL`, `ON_USE` + - Default for new entries: `ON_INSTALL` + - `products` (`array` of `string`, optional): Product override for this plugin entry. Omit it unless product gating is explicitly requested. +- `category` (`string`): Display category bucket. Always include it. + +### Marketplace generation rules + +- `displayName` belongs under the top-level `interface` object, not individual plugin entries. +- When creating a new marketplace file from scratch, seed `interface.displayName` alongside top-level `name`. +- Always include `policy.installation`, `policy.authentication`, and `category` on every generated or updated plugin entry. +- Treat `policy.products` as an override and omit it unless explicitly requested. +- Append new entries unless the user explicitly requests reordering. +- Replace an existing entry for the same plugin only when overwrite is intentional. +- Default new plugin creation to the personal marketplace. +- Use a repo/team marketplace only when the user specifically requests that destination. +- Only override the marketplace `name` when the default `personal` name is already taken or + installed and you need to seed a different new marketplace file. +- Choose marketplace location to match the selected destination: + - Personal plugin: `~/.agents/plugins/marketplace.json` + - Repo/team plugin: `/.agents/plugins/marketplace.json` + +### Plugin validation notes + +- The validator mirrors the workspace plugin ingestion schema so generated plugins follow the same + manifest contract from the start. +- Plugin manifests must include real values for `name`, `version`, `description`, + `author.name`, and the required `interface` fields. +- `version` must use strict semver. +- `websiteURL`, `privacyPolicyURL`, and `termsOfServiceURL` must be absolute `https://` URLs when + present. +- `composerIcon`, `logo`, `logoDark`, and `screenshots` must point to real files inside the plugin archive when + present. +- `apps` should appear in `plugin.json` only when `.app.json` actually exists. +- `mcpServers` may point to `.mcp.json` or contain the MCP server object directly in + `plugin.json`. +- Validation rejects unsupported manifest fields such as `hooks`, so the scaffold keeps them out of + generated manifests. +- Run `scripts/validate_plugin.py ` before handing back a generated plugin. It adds one + intentional preflight check that rejects leftover `[TODO: ...]` placeholders. diff --git a/agent/skills/plugin-creator/scripts/create_basic_plugin.py b/agent/skills/plugin-creator/scripts/create_basic_plugin.py new file mode 100755 index 00000000..78b9fca8 --- /dev/null +++ b/agent/skills/plugin-creator/scripts/create_basic_plugin.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python3 +"""Scaffold a plugin directory and optionally update marketplace.json.""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path +from typing import Any + + +MAX_PLUGIN_NAME_LENGTH = 64 +DEFAULT_INSTALL_POLICY = "AVAILABLE" +DEFAULT_AUTH_POLICY = "ON_INSTALL" +DEFAULT_CATEGORY = "Productivity" +DEFAULT_MARKETPLACE_NAME = "personal" +VALID_INSTALL_POLICIES = {"NOT_AVAILABLE", "AVAILABLE", "INSTALLED_BY_DEFAULT"} +VALID_AUTH_POLICIES = {"ON_INSTALL", "ON_USE"} +DEFAULT_PLUGIN_PARENT = Path.home() / "plugins" +DEFAULT_MARKETPLACE_PATH = Path.home() / ".agents" / "plugins" / "marketplace.json" + + +def normalize_plugin_name(plugin_name: str) -> str: + """Normalize a plugin name to lowercase hyphen-case.""" + normalized = plugin_name.strip().lower() + normalized = re.sub(r"[^a-z0-9]+", "-", normalized) + normalized = normalized.strip("-") + normalized = re.sub(r"-{2,}", "-", normalized) + return normalized + + +def validate_plugin_name(plugin_name: str) -> None: + if not plugin_name: + raise ValueError("Plugin name must include at least one letter or digit.") + if len(plugin_name) > MAX_PLUGIN_NAME_LENGTH: + raise ValueError( + f"Plugin name '{plugin_name}' is too long ({len(plugin_name)} characters). " + f"Maximum is {MAX_PLUGIN_NAME_LENGTH} characters." + ) + + +def validate_marketplace_name(marketplace_name: str) -> None: + if not marketplace_name: + raise ValueError("Marketplace name must include at least one letter or digit.") + if re.fullmatch(r"[A-Za-z0-9_-]+", marketplace_name) is None: + raise ValueError( + "Marketplace name may only contain ASCII letters, digits, `_`, and `-`." + ) + + +def display_name_from_plugin_name(plugin_name: str) -> str: + return " ".join(part.capitalize() for part in re.split(r"[-_]+", plugin_name)) + + +def build_plugin_json(plugin_name: str, *, with_mcp: bool, with_apps: bool) -> dict[str, Any]: + display_name = display_name_from_plugin_name(plugin_name) + payload: dict[str, Any] = { + "name": plugin_name, + "version": "0.1.0", + "description": f"{display_name} plugin", + "author": { + "name": "Local developer", + }, + "skills": "./skills/", + "interface": { + "displayName": display_name, + "shortDescription": f"Use {display_name} in Codex.", + "longDescription": f"{display_name} adds a local Codex plugin scaffold.", + "developerName": "Local developer", + "category": DEFAULT_CATEGORY, + "capabilities": [], + "defaultPrompt": f"Help me use {display_name}.", + }, + } + if with_mcp: + payload["mcpServers"] = "./.mcp.json" + if with_apps: + payload["apps"] = "./.app.json" + return payload + + +def build_marketplace_entry( + plugin_name: str, + install_policy: str, + auth_policy: str, + category: str, +) -> dict[str, Any]: + return { + "name": plugin_name, + "source": { + "source": "local", + "path": f"./plugins/{plugin_name}", + }, + "policy": { + "installation": install_policy, + "authentication": auth_policy, + }, + "category": category, + } + + +def load_json(path: Path) -> dict[str, Any]: + with path.open() as handle: + return json.load(handle) + + +def build_default_marketplace(marketplace_name: str) -> dict[str, Any]: + return { + "name": marketplace_name, + "interface": { + "displayName": display_name_from_plugin_name(marketplace_name), + }, + "plugins": [], + } + + +def validate_marketplace_interface(payload: dict[str, Any]) -> None: + interface = payload.get("interface") + if interface is not None and not isinstance(interface, dict): + raise ValueError("marketplace.json field 'interface' must be an object.") + + +def update_marketplace_json( + marketplace_path: Path, + marketplace_name: str | None, + plugin_name: str, + install_policy: str, + auth_policy: str, + category: str, + force: bool, +) -> None: + if marketplace_path.exists(): + payload = load_json(marketplace_path) + else: + payload = build_default_marketplace(marketplace_name or DEFAULT_MARKETPLACE_NAME) + + if not isinstance(payload, dict): + raise ValueError(f"{marketplace_path} must contain a JSON object.") + + validate_marketplace_interface(payload) + + existing_marketplace_name = payload.get("name") + if marketplace_name is not None: + if not isinstance(existing_marketplace_name, str) or not existing_marketplace_name.strip(): + raise ValueError(f"{marketplace_path} must contain a non-empty string 'name'.") + if existing_marketplace_name != marketplace_name: + raise ValueError( + f"{marketplace_path} already uses marketplace name " + f"'{existing_marketplace_name}'. Create a new marketplace file to use " + f"'{marketplace_name}' instead." + ) + + plugins = payload.setdefault("plugins", []) + if not isinstance(plugins, list): + raise ValueError(f"{marketplace_path} field 'plugins' must be an array.") + + new_entry = build_marketplace_entry(plugin_name, install_policy, auth_policy, category) + + for index, entry in enumerate(plugins): + if isinstance(entry, dict) and entry.get("name") == plugin_name: + if not force: + raise FileExistsError( + f"Marketplace entry '{plugin_name}' already exists in {marketplace_path}. " + "Use --force to overwrite that entry." + ) + plugins[index] = new_entry + break + else: + plugins.append(new_entry) + + write_json(marketplace_path, payload, force=True) + + +def write_json(path: Path, data: dict, force: bool) -> None: + if path.exists() and not force: + raise FileExistsError(f"{path} already exists. Use --force to overwrite.") + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w") as handle: + json.dump(data, handle, indent=2) + handle.write("\n") + + +def create_stub_file(path: Path, payload: dict, force: bool) -> None: + if path.exists() and not force: + return + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w") as handle: + json.dump(payload, handle, indent=2) + handle.write("\n") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Create a plugin skeleton with a validation-ready plugin.json." + ) + parser.add_argument("plugin_name") + parser.add_argument( + "--path", + default=str(DEFAULT_PLUGIN_PARENT), + help=( + "Parent directory for plugin creation (defaults to /plugins). " + "Pass an explicit repo path only when a repo/team plugin is intended." + ), + ) + parser.add_argument("--with-skills", action="store_true", help="Create skills/ directory") + parser.add_argument("--with-hooks", action="store_true", help="Create hooks/ directory") + parser.add_argument("--with-scripts", action="store_true", help="Create scripts/ directory") + parser.add_argument("--with-assets", action="store_true", help="Create assets/ directory") + parser.add_argument("--with-mcp", action="store_true", help="Create .mcp.json placeholder") + parser.add_argument("--with-apps", action="store_true", help="Create .app.json placeholder") + parser.add_argument( + "--with-marketplace", + action="store_true", + help=( + "Create or update /.agents/plugins/marketplace.json by default. " + "Marketplace entries always point to ./plugins/ relative to the " + "marketplace root." + ), + ) + parser.add_argument( + "--marketplace-path", + default=str(DEFAULT_MARKETPLACE_PATH), + help=( + "Path to marketplace.json (defaults to /.agents/plugins/marketplace.json). " + "Pass a repo-rooted marketplace path only when a repo/team plugin is intended." + ), + ) + parser.add_argument( + "--marketplace-name", + help=( + "Marketplace name to seed into a new marketplace.json. Use this only when the default " + "'personal' marketplace name is already taken and you need a different new marketplace." + ), + ) + parser.add_argument( + "--install-policy", + default=DEFAULT_INSTALL_POLICY, + choices=sorted(VALID_INSTALL_POLICIES), + help="Marketplace policy.installation value", + ) + parser.add_argument( + "--auth-policy", + default=DEFAULT_AUTH_POLICY, + choices=sorted(VALID_AUTH_POLICIES), + help="Marketplace policy.authentication value", + ) + parser.add_argument( + "--category", + default=DEFAULT_CATEGORY, + help="Marketplace category value", + ) + parser.add_argument("--force", action="store_true", help="Overwrite existing files") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + raw_plugin_name = args.plugin_name + plugin_name = normalize_plugin_name(raw_plugin_name) + if plugin_name != raw_plugin_name: + print(f"Note: Normalized plugin name from '{raw_plugin_name}' to '{plugin_name}'.") + validate_plugin_name(plugin_name) + marketplace_name = None + if args.marketplace_name is not None: + marketplace_name = args.marketplace_name.strip() + validate_marketplace_name(marketplace_name) + + plugin_root = (Path(args.path).expanduser().resolve() / plugin_name) + plugin_root.mkdir(parents=True, exist_ok=True) + + plugin_json_path = plugin_root / ".codex-plugin" / "plugin.json" + write_json( + plugin_json_path, + build_plugin_json(plugin_name, with_mcp=args.with_mcp, with_apps=args.with_apps), + args.force, + ) + + optional_directories = { + "skills": args.with_skills, + "hooks": args.with_hooks, + "scripts": args.with_scripts, + "assets": args.with_assets, + } + for folder, enabled in optional_directories.items(): + if enabled: + (plugin_root / folder).mkdir(parents=True, exist_ok=True) + + if args.with_mcp: + create_stub_file( + plugin_root / ".mcp.json", + {"mcpServers": {}}, + args.force, + ) + + if args.with_apps: + create_stub_file( + plugin_root / ".app.json", + { + "apps": {}, + }, + args.force, + ) + + if args.with_marketplace: + marketplace_path = Path(args.marketplace_path).expanduser().resolve() + update_marketplace_json( + marketplace_path, + marketplace_name, + plugin_name, + args.install_policy, + args.auth_policy, + args.category, + args.force, + ) + + print(f"Created plugin scaffold: {plugin_root}") + print(f"plugin manifest: {plugin_json_path}") + if args.with_marketplace: + print(f"marketplace manifest: {marketplace_path}") + + +if __name__ == "__main__": + main() diff --git a/agent/skills/plugin-creator/scripts/read_marketplace_name.py b/agent/skills/plugin-creator/scripts/read_marketplace_name.py new file mode 100644 index 00000000..597e9f7a --- /dev/null +++ b/agent/skills/plugin-creator/scripts/read_marketplace_name.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Print the top-level marketplace name from any marketplace.json file.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + + +def default_marketplace_path() -> Path: + return Path.home() / ".agents" / "plugins" / "marketplace.json" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Print the top-level marketplace name from marketplace.json. Defaults to the personal " + "marketplace path under the current home directory." + ) + ) + parser.add_argument( + "--marketplace-path", + default=str(default_marketplace_path()), + help="Path to marketplace.json", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + marketplace_path = Path(args.marketplace_path).expanduser().resolve() + payload = json.loads(marketplace_path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise ValueError(f"{marketplace_path} must contain a JSON object.") + name = payload.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError(f"{marketplace_path} must contain a non-empty string 'name'.") + print(name.strip()) + + +if __name__ == "__main__": + try: + main() + except Exception as err: # noqa: BLE001 - CLI should surface a single clear message. + print(str(err), file=sys.stderr) + raise SystemExit(1) from err diff --git a/agent/skills/plugin-creator/scripts/update_plugin_cachebuster.py b/agent/skills/plugin-creator/scripts/update_plugin_cachebuster.py new file mode 100644 index 00000000..82d19e5d --- /dev/null +++ b/agent/skills/plugin-creator/scripts/update_plugin_cachebuster.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Rewrite a local plugin version to a single Codex cachebuster suffix.""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from datetime import datetime, timezone +from pathlib import Path + + +CACHEBUSTER_PREFIX = "codex" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Rewrite a local plugin's version so it preserves everything before '+' and uses " + "a single +codex. suffix." + ) + ) + parser.add_argument("plugin_path", help="Path to the plugin root directory") + parser.add_argument( + "--cachebuster", + help="Optional cachebuster token to embed in the plugin version", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + plugin_root = Path(args.plugin_path).expanduser().resolve() + manifest_path = plugin_root / ".codex-plugin" / "plugin.json" + manifest = load_manifest(manifest_path) + + version = manifest.get("version") + if not isinstance(version, str) or not version.strip(): + raise ValueError(f"{manifest_path} must contain a non-empty string 'version'.") + cachebuster = sanitize_cachebuster(args.cachebuster or default_cachebuster()) + next_version = with_cachebuster(version, cachebuster) + manifest["version"] = next_version + manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + + print(f"Updated plugin version: {version} -> {next_version}") + + +def load_manifest(manifest_path: Path) -> dict[str, object]: + if not manifest_path.is_file(): + raise FileNotFoundError(f"missing manifest: {manifest_path}") + payload = json.loads(manifest_path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise ValueError(f"{manifest_path} must contain a JSON object.") + return payload +def sanitize_cachebuster(value: str) -> str: + sanitized = re.sub(r"[^a-z0-9-]+", "-", value.strip().lower()) + sanitized = re.sub(r"-{2,}", "-", sanitized).strip("-") + if not sanitized: + raise ValueError("Cachebuster must contain at least one letter or digit.") + return sanitized + + +def default_cachebuster() -> str: + return datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S") + + +def with_cachebuster(version: str, cachebuster: str) -> str: + version_prefix = version.split("+", 1)[0] + return f"{version_prefix}+{CACHEBUSTER_PREFIX}.{cachebuster}" + + +if __name__ == "__main__": + try: + main() + except Exception as err: # noqa: BLE001 - CLI should surface a single clear message. + print(str(err), file=sys.stderr) + raise SystemExit(1) from err diff --git a/agent/skills/plugin-creator/scripts/validate_plugin.py b/agent/skills/plugin-creator/scripts/validate_plugin.py new file mode 100644 index 00000000..88fae0fd --- /dev/null +++ b/agent/skills/plugin-creator/scripts/validate_plugin.py @@ -0,0 +1,629 @@ +#!/usr/bin/env python3 +"""Validate a generated plugin against the plugin ingestion contract.""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path, PurePosixPath +from typing import Any +from urllib.parse import urlparse + +import yaml + + +TODO_MARKER = "[TODO:" +SEMVER_RE = re.compile( + r"^(0|[1-9]\d*)\." + r"(0|[1-9]\d*)\." + r"(0|[1-9]\d*)" + r"(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\." + r"(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?" + r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$" +) +HEX_COLOR_RE = re.compile(r"^#[0-9A-F]{6}$", re.IGNORECASE) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Validate a local Codex plugin.") + parser.add_argument("plugin_path", help="Path to the plugin root directory") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + plugin_root = Path(args.plugin_path).expanduser().resolve() + errors = validate_plugin(plugin_root) + if errors: + print("Plugin validation failed:") + for error in errors: + print(f"- {error}") + raise SystemExit(1) + print(f"Plugin validation passed: {plugin_root}") + + +def validate_plugin(plugin_root: Path) -> list[str]: + errors: list[str] = [] + manifest_path = plugin_root / ".codex-plugin" / "plugin.json" + manifest = load_json_object(manifest_path, errors) + if manifest is None: + return errors + + reject_todo_markers(manifest, "$", errors) + validate_manifest_shape(plugin_root, manifest, errors) + return errors + + +def load_json_object(path: Path, errors: list[str]) -> dict[str, Any] | None: + if not path.is_file(): + errors.append("missing `.codex-plugin/plugin.json`") + return None + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except OSError: + errors.append("unable to read `.codex-plugin/plugin.json`") + return None + except json.JSONDecodeError: + errors.append("`.codex-plugin/plugin.json` must be valid JSON") + return None + if not isinstance(payload, dict): + errors.append("`.codex-plugin/plugin.json` must contain a JSON object") + return None + return payload + + +def reject_todo_markers(value: Any, path: str, errors: list[str]) -> None: + if isinstance(value, str): + if TODO_MARKER in value: + errors.append(f"{path} still contains a `[TODO: ...]` placeholder") + return + if isinstance(value, list): + for index, item in enumerate(value): + reject_todo_markers(item, f"{path}[{index}]", errors) + return + if isinstance(value, dict): + for key, item in value.items(): + reject_todo_markers(item, f"{path}.{key}", errors) + + +def validate_manifest_shape( + plugin_root: Path, + manifest: dict[str, Any], + errors: list[str], +) -> None: + allowed_keys = { + "id", + "name", + "version", + "description", + "skills", + "apps", + "mcpServers", + "interface", + "author", + "homepage", + "repository", + "license", + "keywords", + } + for key in sorted(set(manifest) - allowed_keys): + errors.append(f"plugin.json field `{key}` is not accepted by plugin validation") + + validate_optional_non_empty_string(manifest, "id", errors) + require_non_empty_string(manifest, "name", errors) + version = require_non_empty_string(manifest, "version", errors) + if version is not None and SEMVER_RE.fullmatch(version) is None: + errors.append("plugin.json field `version` must be strict semver") + require_non_empty_string(manifest, "description", errors) + + author = require_object(manifest, "author", errors) + if author is not None: + reject_unknown_fields(author, {"name", "email", "url"}, "author", errors) + require_non_empty_string(author, "name", errors, prefix="author") + validate_optional_non_empty_string(author, "email", errors, prefix="author") + validate_optional_https_url(author, "url", errors, prefix="author") + + validate_optional_contract_path(manifest, "skills", "skills", errors) + validate_optional_contract_path(manifest, "apps", ".app.json", errors) + validate_manifest_mcp_servers(plugin_root, manifest, errors) + + if manifest.get("apps") is not None: + validate_app_manifest( + plugin_root / ".app.json", + errors, + ) + validate_skill_manifests(plugin_root, errors) + + interface = require_object(manifest, "interface", errors) + if interface is None: + return + reject_unknown_fields( + interface, + { + "displayName", + "shortDescription", + "longDescription", + "developerName", + "category", + "capabilities", + "websiteURL", + "privacyPolicyURL", + "termsOfServiceURL", + "brandColor", + "composerIcon", + "logo", + "logoDark", + "screenshots", + "defaultPrompt", + "default_prompt", + }, + "interface", + errors, + ) + for field in ( + "displayName", + "shortDescription", + "longDescription", + "developerName", + "category", + ): + require_non_empty_string(interface, field, errors, prefix="interface") + if "defaultPrompt" not in interface and "default_prompt" not in interface: + errors.append( + "plugin.json field `interface.defaultPrompt` or `interface.default_prompt` is required" + ) + capabilities = interface.get("capabilities") + if not isinstance(capabilities, list) or not all( + isinstance(value, str) and value.strip() for value in capabilities + ): + errors.append("plugin.json field `interface.capabilities` must be an array of strings") + for field in ("websiteURL", "privacyPolicyURL", "termsOfServiceURL"): + validate_optional_https_url(interface, field, errors, prefix="interface") + brand_color = interface.get("brandColor") + if brand_color is not None and ( + not isinstance(brand_color, str) or HEX_COLOR_RE.fullmatch(brand_color) is None + ): + errors.append("plugin.json field `interface.brandColor` must use `#RRGGBB`") + for field in ("composerIcon", "logo", "logoDark"): + validate_optional_asset_path(plugin_root, plugin_root, interface, field, errors) + screenshots = interface.get("screenshots", []) + if not isinstance(screenshots, list): + errors.append("plugin.json field `interface.screenshots` must be an array") + else: + for index, raw_path in enumerate(screenshots): + validate_asset_path( + plugin_root, + plugin_root, + raw_path, + f"interface.screenshots[{index}]", + errors, + ) + + +def require_object( + payload: dict[str, Any], + key: str, + errors: list[str], +) -> dict[str, Any] | None: + value = payload.get(key) + if not isinstance(value, dict): + errors.append(f"plugin.json field `{key}` must be an object") + return None + return value + + +def require_non_empty_string( + payload: dict[str, Any], + key: str, + errors: list[str], + *, + prefix: str | None = None, +) -> str | None: + value = payload.get(key) + field = f"{prefix}.{key}" if prefix is not None else key + if not isinstance(value, str) or not value.strip(): + errors.append(f"plugin.json field `{field}` must be a non-empty string") + return None + return value + + +def validate_optional_non_empty_string( + payload: dict[str, Any], + key: str, + errors: list[str], + *, + prefix: str | None = None, +) -> None: + value = payload.get(key) + if value is None: + return + field = f"{prefix}.{key}" if prefix is not None else key + if not isinstance(value, str) or not value.strip(): + errors.append(f"plugin.json field `{field}` must be a non-empty string") + + +def reject_unknown_fields( + payload: dict[str, Any], + allowed_keys: set[str], + prefix: str, + errors: list[str], +) -> None: + for key in sorted(set(payload) - allowed_keys): + errors.append(f"plugin.json field `{prefix}.{key}` is not accepted by plugin validation") + + +def validate_optional_https_url( + payload: dict[str, Any], + key: str, + errors: list[str], + *, + prefix: str, +) -> None: + value = payload.get(key) + if value is None: + return + parsed = urlparse(value) if isinstance(value, str) else None + if parsed is None or parsed.scheme != "https" or not parsed.netloc: + errors.append(f"plugin.json field `{prefix}.{key}` must be an absolute `https://` URL") + + +def validate_optional_contract_path( + payload: dict[str, Any], + key: str, + expected: str, + errors: list[str], +) -> None: + value = payload.get(key) + if value is None: + return + normalized = normalize_contract_path(value) if isinstance(value, str) else None + if normalized != expected: + errors.append(f"plugin.json field `{key}` must resolve to `{expected}`") + + +def validate_manifest_mcp_servers( + plugin_root: Path, + manifest: dict[str, Any], + errors: list[str], +) -> None: + value = manifest.get("mcpServers") + if value is None: + return + if isinstance(value, str): + validate_optional_contract_path(manifest, "mcpServers", ".mcp.json", errors) + validate_mcp_manifest( + plugin_root / ".mcp.json", + errors, + ) + return + if isinstance(value, dict): + validate_mcp_server_entries( + value, + "plugin.json field `mcpServers`", + "plugin.json field `mcpServers`", + errors, + ) + return + errors.append("plugin.json field `mcpServers` must be a string path or object") + + +def normalize_contract_path(raw_path: str) -> str | None: + path = Path(raw_path) + if path.is_absolute(): + return None + normalized = path.as_posix().rstrip("/") + return normalized or None + + +def validate_app_manifest(path: Path, errors: list[str]) -> None: + payload = load_companion_json_object(path, "`.app.json`", errors) + if payload is None: + return + reject_companion_unknown_fields(payload, {"apps"}, "`.app.json`", errors) + apps = payload.get("apps") + if not isinstance(apps, dict): + errors.append("`.app.json` field `apps` must be an object") + return + for key, value in apps.items(): + if not isinstance(value, dict): + errors.append(f"`.app.json` app `{key}` must be an object") + continue + reject_companion_unknown_fields( + value, {"id", "category"}, f"`.app.json` app `{key}`", errors + ) + app_id = value.get("id") + if not isinstance(app_id, str) or not app_id.strip(): + errors.append(f"`.app.json` app `{key}` field `id` must be a non-empty string") + category = value.get("category") + if category is not None and (not isinstance(category, str) or not category.strip()): + errors.append( + f"`.app.json` app `{key}` field `category` must be a non-empty string" + ) + + +def validate_mcp_manifest(path: Path, errors: list[str]) -> None: + payload = load_companion_json_object(path, "`.mcp.json`", errors) + if payload is None: + return + reject_companion_unknown_fields(payload, {"mcpServers"}, "`.mcp.json`", errors) + servers = payload.get("mcpServers") + validate_mcp_server_entries( + servers, + "`.mcp.json`", + "`.mcp.json` field `mcpServers`", + errors, + ) + + +def validate_mcp_server_entries( + servers: Any, + source_label: str, + field_label: str, + errors: list[str], +) -> None: + if not isinstance(servers, dict): + errors.append(f"{field_label} must be an object") + return + for key, value in servers.items(): + if not isinstance(key, str) or not key.strip(): + errors.append(f"{source_label} server names must be non-empty strings") + if not isinstance(value, dict): + errors.append(f"{source_label} server `{key}` must be an object") + + +def load_companion_json_object( + path: Path, + label: str, + errors: list[str], +) -> dict[str, Any] | None: + if not path.is_file(): + errors.append(f"{label} is required when its plugin.json field is present") + return None + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + errors.append(f"{label} must contain valid JSON") + return None + if not isinstance(payload, dict): + errors.append(f"{label} must contain a JSON object") + return None + return payload + + +def reject_companion_unknown_fields( + payload: dict[str, Any], + allowed_keys: set[str], + prefix: str, + errors: list[str], +) -> None: + for key in sorted(set(payload) - allowed_keys): + errors.append(f"{prefix} field `{key}` is not accepted by plugin validation") + + +def validate_skill_manifests(plugin_root: Path, errors: list[str]) -> None: + skills_root = plugin_root / "skills" + if not skills_root.is_dir(): + return + for skill_root in sorted(skills_root.iterdir(), key=lambda path: path.name): + if skill_root.name.startswith(".") or not skill_root.is_dir(): + continue + validate_skill_manifest(skill_root, errors) + + +def validate_skill_manifest(skill_root: Path, errors: list[str]) -> None: + skill_md_path = skill_root / "SKILL.md" + if not skill_md_path.is_file(): + errors.append(f"skill `{skill_root.name}` is missing `SKILL.md`") + return + try: + contents = skill_md_path.read_text(encoding="utf-8") + except OSError: + errors.append(f"unable to read skill `{skill_root.name}`") + return + if not contents.startswith("---\n"): + errors.append(f"skill `{skill_root.name}` must start with YAML frontmatter") + return + frontmatter_end = contents.find("\n---", 4) + if frontmatter_end == -1: + errors.append(f"skill `{skill_root.name}` frontmatter is not closed") + return + try: + frontmatter = yaml.safe_load(contents[4:frontmatter_end]) + except yaml.YAMLError: + errors.append(f"skill `{skill_root.name}` frontmatter must be valid YAML") + return + if not isinstance(frontmatter, dict): + errors.append(f"skill `{skill_root.name}` frontmatter must be an object") + return + skill_name = frontmatter.get("name") + if not isinstance(skill_name, str) or not skill_name.strip(): + errors.append(f"skill `{skill_root.name}` frontmatter field `name` must be non-empty") + description = frontmatter.get("description") + if not isinstance(description, str) or not description.strip(): + errors.append( + f"skill `{skill_root.name}` frontmatter field `description` must be non-empty" + ) + disable_model_invocation = frontmatter.get("disable-model-invocation") + if disable_model_invocation is None: + disable_model_invocation = frontmatter.get("disable_model_invocation") + if disable_model_invocation not in (None, False): + errors.append( + f"skill `{skill_root.name}` frontmatter field `disable-model-invocation` must be false" + ) + agent_yaml_path = skill_root / "agents" / "openai.yaml" + if agent_yaml_path.is_file(): + validate_skill_agent_manifest( + plugin_root=skill_root.parent.parent, + skill_root=skill_root, + agent_yaml_path=agent_yaml_path, + errors=errors, + ) + + +def validate_skill_agent_manifest( + *, + plugin_root: Path, + skill_root: Path, + agent_yaml_path: Path, + errors: list[str], +) -> None: + try: + payload = yaml.safe_load(agent_yaml_path.read_text(encoding="utf-8")) + except OSError: + errors.append(f"unable to read skill `{skill_root.name}` agent YAML") + return + except yaml.YAMLError: + errors.append(f"skill `{skill_root.name}` agent YAML must be valid YAML") + return + if not isinstance(payload, dict): + errors.append(f"skill `{skill_root.name}` agent YAML must be an object") + return + + reject_skill_agent_unknown_fields( + payload, + {"interface", "policy", "dependencies"}, + skill_root, + errors, + ) + interface = payload.get("interface") + if not isinstance(interface, dict): + errors.append(f"skill `{skill_root.name}` agent field `interface` must be an object") + return + reject_skill_agent_unknown_fields( + interface, + { + "display_name", + "short_description", + "icon_small", + "icon_large", + "brand_color", + "default_prompt", + }, + skill_root, + errors, + prefix="interface", + ) + for field in ("display_name", "short_description"): + value = interface.get(field) + if not isinstance(value, str) or not value.strip(): + errors.append( + f"skill `{skill_root.name}` agent field `interface.{field}` must be non-empty" + ) + for field in ("icon_small", "icon_large"): + validate_optional_asset_path( + skill_root, + plugin_root, + interface, + field, + errors, + prefix=f"skill `{skill_root.name}` agent field `interface", + ) + brand_color = interface.get("brand_color") + if brand_color is not None and ( + not isinstance(brand_color, str) or HEX_COLOR_RE.fullmatch(brand_color) is None + ): + errors.append( + f"skill `{skill_root.name}` agent field `interface.brand_color` must use `#RRGGBB`" + ) + default_prompt = interface.get("default_prompt") + if default_prompt is not None and ( + not isinstance(default_prompt, str) or not default_prompt.strip() + ): + errors.append( + f"skill `{skill_root.name}` agent field `interface.default_prompt` must be non-empty" + ) + + policy = payload.get("policy") + if policy is not None: + if not isinstance(policy, dict): + errors.append(f"skill `{skill_root.name}` agent field `policy` must be an object") + else: + reject_skill_agent_unknown_fields( + policy, + {"allow_implicit_invocation"}, + skill_root, + errors, + prefix="policy", + ) + allow_implicit_invocation = policy.get("allow_implicit_invocation") + if allow_implicit_invocation is not None and not isinstance( + allow_implicit_invocation, + bool, + ): + errors.append( + f"skill `{skill_root.name}` agent field " + "`policy.allow_implicit_invocation` must be a boolean" + ) + + dependencies = payload.get("dependencies") + if dependencies is not None: + if not isinstance(dependencies, dict): + errors.append( + f"skill `{skill_root.name}` agent field `dependencies` must be an object" + ) + else: + reject_skill_agent_unknown_fields( + dependencies, + {"tools"}, + skill_root, + errors, + prefix="dependencies", + ) + + +def reject_skill_agent_unknown_fields( + payload: dict[str, Any], + allowed_keys: set[str], + skill_root: Path, + errors: list[str], + *, + prefix: str | None = None, +) -> None: + for key in sorted(set(payload) - allowed_keys): + field = f"{prefix}.{key}" if prefix is not None else key + errors.append( + f"skill `{skill_root.name}` agent field `{field}` is not accepted by plugin validation" + ) + + +def validate_optional_asset_path( + base_dir: Path, + allowed_root: Path, + payload: dict[str, Any], + key: str, + errors: list[str], + *, + prefix: str = "interface", +) -> None: + raw_path = payload.get(key) + if raw_path is None: + return + validate_asset_path(base_dir, allowed_root, raw_path, f"{prefix}.{key}", errors) + + +def validate_asset_path( + base_dir: Path, + allowed_root: Path, + raw_path: Any, + field: str, + errors: list[str], +) -> None: + label = field if field.startswith("skill `") else f"plugin.json field `{field}`" + if not isinstance(raw_path, str) or not raw_path.strip(): + errors.append(f"{label} must be a non-empty relative path") + return + candidate = PurePosixPath(raw_path.replace("\\", "/")) + if candidate.is_absolute() or any(part in {"", ".", ".."} for part in candidate.parts): + errors.append(f"{label} must stay inside the plugin archive") + return + resolved_path = (base_dir / candidate.as_posix()).resolve() + if not resolved_path.is_relative_to(allowed_root.resolve()): + errors.append(f"{label} must stay inside the plugin archive") + return + if not resolved_path.is_file(): + errors.append(f"{label} points to a missing file") + + +if __name__ == "__main__": + main() diff --git a/plugins/antfu/.claude/skills/antfu b/plugins/antfu/.claude/skills/antfu new file mode 120000 index 00000000..4fc00718 --- /dev/null +++ b/plugins/antfu/.claude/skills/antfu @@ -0,0 +1 @@ +../../.agents/skills/antfu \ No newline at end of file diff --git a/plugins/ast-grep/.claude/skills/ast-grep b/plugins/ast-grep/.claude/skills/ast-grep new file mode 120000 index 00000000..00582958 --- /dev/null +++ b/plugins/ast-grep/.claude/skills/ast-grep @@ -0,0 +1 @@ +../../.agents/skills/ast-grep \ No newline at end of file diff --git a/plugins/axi/.claude/skills/axi b/plugins/axi/.claude/skills/axi new file mode 120000 index 00000000..51a5e828 --- /dev/null +++ b/plugins/axi/.claude/skills/axi @@ -0,0 +1 @@ +../../.agents/skills/axi \ No newline at end of file diff --git a/plugins/better-auth/.agents/skills/better-auth-best-practices/SKILL.md b/plugins/better-auth/.agents/skills/better-auth-best-practices/SKILL.md index 3e6a4e19..ddeedc81 100644 --- a/plugins/better-auth/.agents/skills/better-auth-best-practices/SKILL.md +++ b/plugins/better-auth/.agents/skills/better-auth-best-practices/SKILL.md @@ -15,7 +15,10 @@ description: Configure Better Auth server and client, set up database adapters, 2. Set env vars: `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` 3. Create `auth.ts` with database + config 4. Create route handler for your framework -5. Run `npx @better-auth/cli@latest migrate` +5. Run migrations: + - **Built-in adapter:** `npx @better-auth/cli@latest migrate` + - **Drizzle:** `npx @better-auth/cli@latest generate --output src/db/auth-schema.ts` then `npx drizzle-kit push` (dev) or `npx drizzle-kit generate && npx drizzle-kit migrate` (prod) + - **Prisma:** `npx @better-auth/cli@latest generate --output prisma/schema.prisma` then `npx prisma migrate dev` 6. Verify: call `GET /api/auth/ok` — should return `{ status: "ok" }` --- @@ -59,10 +62,12 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c ## Database -**Direct connections:** Pass `pg.Pool`, `mysql2` pool, `better-sqlite3`, or `bun:sqlite` instance. +**Direct connections:** Pass `pg.Pool`, `mysql2` pool, `better-sqlite3`, or `bun:sqlite` instance. For Postgres, also supports `postgres` (postgres.js) and `@neondatabase/serverless`. **ORM adapters:** Import from `better-auth/adapters/drizzle`, `better-auth/adapters/prisma`, `better-auth/adapters/mongodb`. +**Drizzle provider values:** `"pg"` (PostgreSQL), `"mysql"` (MySQL), `"sqlite"` (SQLite). Must match the driver used. + **Critical:** Better Auth uses adapter model names, NOT underlying table names. If Prisma model is `User` mapping to table `users`, use `modelName: "user"` (Prisma reference), not `"users"`. --- @@ -163,6 +168,8 @@ For separate client/server projects: `createAuthClient()`. 4. **Cookie cache** - Custom session fields NOT cached, always re-fetched 5. **Stateless mode** - No DB = session in cookie only, logout on cache expiry 6. **Change email flow** - Sends to current email first, then new email +7. **Drizzle: db not initialized** - `drizzleAdapter(db, ...)` requires a `db` instance from `drizzle()`. See `create-auth` skill for setup examples (node-postgres, postgres.js, Neon). +8. **Drizzle: missing drizzle.config.ts** - `drizzle-kit` commands require a `drizzle.config.ts` pointing to the generated schema file and DB credentials. --- diff --git a/plugins/better-auth/.agents/skills/organization-best-practices/SKILL.md b/plugins/better-auth/.agents/skills/organization-best-practices/SKILL.md index 0e84a844..6f2f6447 100644 --- a/plugins/better-auth/.agents/skills/organization-best-practices/SKILL.md +++ b/plugins/better-auth/.agents/skills/organization-best-practices/SKILL.md @@ -7,7 +7,7 @@ description: Configure multi-tenant organizations, manage members and invitation 1. Add `organization()` plugin to server config 2. Add `organizationClient()` plugin to client config -3. Run `npx @better-auth/cli migrate` +3. Run `npx @better-auth/cli@latest migrate` (built-in adapter) or generate + push for Drizzle/Prisma 4. Verify: check that organization, member, invitation tables exist in your database ```ts diff --git a/plugins/better-auth/.agents/skills/two-factor-authentication-best-practices/SKILL.md b/plugins/better-auth/.agents/skills/two-factor-authentication-best-practices/SKILL.md index cf9c30bf..74acac84 100644 --- a/plugins/better-auth/.agents/skills/two-factor-authentication-best-practices/SKILL.md +++ b/plugins/better-auth/.agents/skills/two-factor-authentication-best-practices/SKILL.md @@ -7,7 +7,7 @@ description: Configure TOTP authenticator apps, send OTP codes via email/SMS, ma 1. Add `twoFactor()` plugin to server config with `issuer` 2. Add `twoFactorClient()` plugin to client config -3. Run `npx @better-auth/cli migrate` +3. Run `npx @better-auth/cli@latest migrate` (built-in adapter) or generate + push for Drizzle/Prisma 4. Verify: check that `twoFactorSecret` column exists on user table ```ts diff --git a/plugins/better-auth/.claude/skills/better-auth-best-practices b/plugins/better-auth/.claude/skills/better-auth-best-practices new file mode 120000 index 00000000..d28d6bd7 --- /dev/null +++ b/plugins/better-auth/.claude/skills/better-auth-best-practices @@ -0,0 +1 @@ +../../.agents/skills/better-auth-best-practices \ No newline at end of file diff --git a/plugins/better-auth/.claude/skills/email-and-password-best-practices b/plugins/better-auth/.claude/skills/email-and-password-best-practices new file mode 120000 index 00000000..e78bcf65 --- /dev/null +++ b/plugins/better-auth/.claude/skills/email-and-password-best-practices @@ -0,0 +1 @@ +../../.agents/skills/email-and-password-best-practices \ No newline at end of file diff --git a/plugins/better-auth/.claude/skills/organization-best-practices b/plugins/better-auth/.claude/skills/organization-best-practices new file mode 120000 index 00000000..d43ca82a --- /dev/null +++ b/plugins/better-auth/.claude/skills/organization-best-practices @@ -0,0 +1 @@ +../../.agents/skills/organization-best-practices \ No newline at end of file diff --git a/plugins/better-auth/.claude/skills/two-factor-authentication-best-practices b/plugins/better-auth/.claude/skills/two-factor-authentication-best-practices new file mode 120000 index 00000000..0defcaa0 --- /dev/null +++ b/plugins/better-auth/.claude/skills/two-factor-authentication-best-practices @@ -0,0 +1 @@ +../../.agents/skills/two-factor-authentication-best-practices \ No newline at end of file diff --git a/plugins/better-auth/agent/skills/better-auth-best-practices/SKILL.md b/plugins/better-auth/agent/skills/better-auth-best-practices/SKILL.md index b380767c..3a77bc34 100644 --- a/plugins/better-auth/agent/skills/better-auth-best-practices/SKILL.md +++ b/plugins/better-auth/agent/skills/better-auth-best-practices/SKILL.md @@ -13,7 +13,10 @@ description: "Configure Better Auth server and client, set up database adapters, 2. Set env vars: `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` 3. Create `auth.ts` with database + config 4. Create route handler for your framework -5. Run `npx @better-auth/cli@latest migrate` +5. Run migrations: + - **Built-in adapter:** `npx @better-auth/cli@latest migrate` + - **Drizzle:** `npx @better-auth/cli@latest generate --output src/db/auth-schema.ts` then `npx drizzle-kit push` (dev) or `npx drizzle-kit generate && npx drizzle-kit migrate` (prod) + - **Prisma:** `npx @better-auth/cli@latest generate --output prisma/schema.prisma` then `npx prisma migrate dev` 6. Verify: call `GET /api/auth/ok` — should return `{ status: "ok" }` --- @@ -57,10 +60,12 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c ## Database -**Direct connections:** Pass `pg.Pool`, `mysql2` pool, `better-sqlite3`, or `bun:sqlite` instance. +**Direct connections:** Pass `pg.Pool`, `mysql2` pool, `better-sqlite3`, or `bun:sqlite` instance. For Postgres, also supports `postgres` (postgres.js) and `@neondatabase/serverless`. **ORM adapters:** Import from `better-auth/adapters/drizzle`, `better-auth/adapters/prisma`, `better-auth/adapters/mongodb`. +**Drizzle provider values:** `"pg"` (PostgreSQL), `"mysql"` (MySQL), `"sqlite"` (SQLite). Must match the driver used. + **Critical:** Better Auth uses adapter model names, NOT underlying table names. If Prisma model is `User` mapping to table `users`, use `modelName: "user"` (Prisma reference), not `"users"`. --- @@ -161,6 +166,8 @@ For separate client/server projects: `createAuthClient()`. 4. **Cookie cache** - Custom session fields NOT cached, always re-fetched 5. **Stateless mode** - No DB = session in cookie only, logout on cache expiry 6. **Change email flow** - Sends to current email first, then new email +7. **Drizzle: db not initialized** - `drizzleAdapter(db, ...)` requires a `db` instance from `drizzle()`. See `create-auth` skill for setup examples (node-postgres, postgres.js, Neon). +8. **Drizzle: missing drizzle.config.ts** - `drizzle-kit` commands require a `drizzle.config.ts` pointing to the generated schema file and DB credentials. --- diff --git a/plugins/better-auth/agent/skills/organization-best-practices/SKILL.md b/plugins/better-auth/agent/skills/organization-best-practices/SKILL.md index 4e07fbbd..819089a9 100644 --- a/plugins/better-auth/agent/skills/organization-best-practices/SKILL.md +++ b/plugins/better-auth/agent/skills/organization-best-practices/SKILL.md @@ -5,7 +5,7 @@ description: "Configure multi-tenant organizations, manage members and invitatio 1. Add `organization()` plugin to server config 2. Add `organizationClient()` plugin to client config -3. Run `npx @better-auth/cli migrate` +3. Run `npx @better-auth/cli@latest migrate` (built-in adapter) or generate + push for Drizzle/Prisma 4. Verify: check that organization, member, invitation tables exist in your database ```ts diff --git a/plugins/better-auth/agent/skills/two-factor-authentication-best-practices/SKILL.md b/plugins/better-auth/agent/skills/two-factor-authentication-best-practices/SKILL.md index c5bdf15a..6f366f3c 100644 --- a/plugins/better-auth/agent/skills/two-factor-authentication-best-practices/SKILL.md +++ b/plugins/better-auth/agent/skills/two-factor-authentication-best-practices/SKILL.md @@ -5,7 +5,7 @@ description: "Configure TOTP authenticator apps, send OTP codes via email/SMS, m 1. Add `twoFactor()` plugin to server config with `issuer` 2. Add `twoFactorClient()` plugin to client config -3. Run `npx @better-auth/cli migrate` +3. Run `npx @better-auth/cli@latest migrate` (built-in adapter) or generate + push for Drizzle/Prisma 4. Verify: check that `twoFactorSecret` column exists on user table ```ts diff --git a/plugins/better-auth/skills-lock.json b/plugins/better-auth/skills-lock.json index 11f6218b..052584f7 100644 --- a/plugins/better-auth/skills-lock.json +++ b/plugins/better-auth/skills-lock.json @@ -5,7 +5,7 @@ "source": "better-auth/skills", "sourceType": "github", "skillPath": "better-auth/best-practices/SKILL.md", - "computedHash": "a4c830509e85557b59339d8d93a4e243e9e59c686e7678854d39230e12c2a6dc" + "computedHash": "61ba0ef64ed2e7c424401cc848ca33dd6d790a720c44727717dc0c5cba5fc122" }, "create-auth-skill": { "source": "better-auth/skills", @@ -23,13 +23,13 @@ "source": "better-auth/skills", "sourceType": "github", "skillPath": "better-auth/organization/SKILL.md", - "computedHash": "79a5a85b43d10e9fe37582b3506b051a33a991817b938f349099baa5ddba21aa" + "computedHash": "27627eb3a13bd44eff3a5d890f96e7db052b623db90e01684f759abe3e7fe56b" }, "two-factor-authentication-best-practices": { "source": "better-auth/skills", "sourceType": "github", "skillPath": "better-auth/twoFactor/SKILL.md", - "computedHash": "7e297aaf887e11fdc03e52bdbf44974e161e8f7c393170ced7dc66cace4f6d46" + "computedHash": "a6f720042e5a090909e0d519a6a50a7eb8179553cd5315f0bbad852a6d20dac8" } } } diff --git a/plugins/dev3000/.agents/skills/d3k/PUBLISH.md b/plugins/dev3000/.agents/skills/d3k/PUBLISH.md index 254bb3dd..512c50ee 100644 --- a/plugins/dev3000/.agents/skills/d3k/PUBLISH.md +++ b/plugins/dev3000/.agents/skills/d3k/PUBLISH.md @@ -13,15 +13,15 @@ ### Short description -`Bootstraps d3k runtime for standalone AI apps` +`Agent-owned local web debugging with a managed browser` ### Long description -`Installs/initializes dev3000 (d3k) for standalone agent shells (Codex, Cursor, Claude Code), starts d3k as the default runtime, and uses unified logs plus CDP browser control instead of raw npm/bun dev.` +`Starts or reuses d3k in a retained background agent session, opens the project-stable managed browser, and uses unified browser/server evidence instead of raw dev servers or separate automation browsers.` ### Default prompt -`Use $d3k to initialize d3k, start the correct runtime, and drive debugging with unified logs and CDP browser controls.` +`Use $d3k to let me test this project in its monitored browser, then inspect the captured evidence after I reproduce the issue.` ## Source URL diff --git a/plugins/dev3000/.agents/skills/d3k/SKILL.md b/plugins/dev3000/.agents/skills/d3k/SKILL.md index 7ae3c655..1439e9ad 100644 --- a/plugins/dev3000/.agents/skills/d3k/SKILL.md +++ b/plugins/dev3000/.agents/skills/d3k/SKILL.md @@ -1,199 +1,128 @@ --- name: "d3k" -description: "Bootstrap d3k in standalone AI apps (Codex, Cursor, Claude Code): detect/install dev3000, start d3k as the runtime, and use unified logs plus d3k-owned browser/session control instead of running npm/bun dev directly." +description: "Use when the user asks to use d3k, run/dev/test/debug a web project with d3k, or reproduce a browser issue. Own the runtime: reuse or background-start d3k non-interactively, wait for readiness, use its project-stable managed Chrome profile, and inspect unified browser/server evidence." --- -# d3k Standalone Bootstrap +# d3k Agent Runtime -Use this skill when working in a standalone AI app and you need reliable local web debugging with browser + server context. +d3k is the local web runtime for this task. It starts the dev server behind a stable Portless URL, owns a project-stable Chrome profile, and records server logs, browser console output, network activity, interactions, and screenshots in one timeline. -## Why d3k-first +When this skill triggers, operate d3k. Do not merely tell the user how to run it. -- `d3k` captures server logs, browser console, network events, and screenshots in one timeline. -- `d3k` owns the browser session so the agent can control the same browser being monitored. -- Running `npm run dev` or `bun run dev` directly omits this unified telemetry and usually leads to weaker diagnoses. +## Interpret the Request -## Auth-Sensitive Browser Rule +- "Let me test/dev my project with d3k": prepare the runtime and headed browser, confirm it is ready, then hand control to the user. Wait for them to reproduce the issue before inspecting evidence. +- "Test/debug/fix this with d3k": prepare the runtime, then drive the managed browser and investigate autonomously. +- If ambiguous, start the runtime and browser first. That action is safe and useful for either path. -For Google OAuth, Supabase auth, and any other auth-sensitive debugging, d3k must own browser startup. Start d3k normally so it launches the app and browser together, including `--app-url` when the target URL is known. +## Start or Reuse d3k -Do not use `d3k agent-browser --profile ... --headed open ...`, raw Chrome, Playwright, browser MCP sessions, manual CDP attachment, or any other separate automation browser for auth debugging unless the user explicitly asks for that path. Agent-browser-created/custom Chrome profiles can be rejected by Google with `This browser or app may not be secure`. +Run from the project root. + +1. Check for an existing project runtime: -After d3k has launched the browser, use the safe managed-browser path: ```bash -d3k agent-browser --require-d3k-browser open "" -d3k agent-browser snapshot -i -d3k agent-browser click @e1 -d3k errors --context +d3k status --json ``` -If this fails because no d3k-managed browser exists, restart d3k cleanly with its normal browser-owning flow. Do not fall back to creating a new agent-browser Chrome for auth. +If it reports `"running": true`, reuse it. Do not start a second dev server or browser. -## Bootstrap Workflow - -1. Confirm whether `d3k` is installed: -```bash -command -v d3k >/dev/null && d3k --version -``` +2. If d3k is not installed, install it: -2. If `d3k` is missing, install dev3000 globally (prefer Bun): ```bash bun install -g dev3000 ``` -Fallback if Bun is unavailable: -```bash -npm install -g dev3000 -``` -3. Start d3k as the runtime and let d3k own browser startup. When the app command, port, and target URL are known, use the normal app-debugging shape: -```bash -d3k --no-agent --command "" --port --startup-timeout --no-tui --app-url "" -``` +Use `npm install -g dev3000` only when Bun is unavailable. - For a repo-default shell with no target URL yet: -```bash -d3k --no-agent --no-tui -t -``` - -4. Keep d3k running while editing code. Do not start a second dev server with `npm/bun dev`. +3. Start d3k with the agent's shell/process tool as a retained background or yielded session: -5. Drive the page through d3k's active browser session: ```bash -d3k agent-browser snapshot -i -d3k agent-browser click @e1 -d3k errors --context +d3k --no-agent --no-tui -t ``` -## Required Browser/Session Default - -When a user asks to start or debug an app with d3k, prefer d3k's normal browser-owning flow, including `--app-url` when a target URL is known. +Do not wait for this long-running command to exit. Keep its process/session handle so you can monitor or stop it later. Prefer the execution tool's background/session support over shelling with `&`. -Do not launch a separate raw Chrome, Playwright browser, browser MCP session, or manually attach to CDP unless the user explicitly asks for that path. Separate automation-only browser profiles can break OAuth flows, especially Google sign-in with `This browser or app may not be secure`. +If the target URL is already known, pass it so the managed browser opens there: -After d3k is running, drive the page through `d3k agent-browser ...` commands so interactions target d3k's active browser session. - -If profile or daemon state seems stale, first run: ```bash -d3k agent-browser close --all +d3k --no-agent --no-tui -t --app-url "" ``` -Then restart d3k cleanly with the normal browser-owning flow. +Let d3k auto-detect the package manager, dev command, and port. Add `--command`, `--script`, or `--port` only when detection is wrong or the user specified them. + +4. Poll until the runtime is ready: -For a normal app-debugging session, use: ```bash -d3k --no-agent --command "" --port --startup-timeout --no-tui --app-url "" -d3k agent-browser --require-d3k-browser open "" -d3k agent-browser snapshot -i -d3k agent-browser click @e1 -d3k errors --context +d3k status --json ``` -## Non-Auth Fresh Browser/Profile Startup - -Use this special-case workflow only for non-auth debugging when the user explicitly asks Codex to start d3k with a fresh browser/profile. Do not use this workflow for Google OAuth, Supabase auth, or any sign-in flow that may reject automation browsers. The default app-debugging workflow is to let d3k own browser startup and then interact through `d3k agent-browser`. +A successful status response is the readiness boundary. Prefer the reported Portless `appUrl`; the underlying app port may change between runs. If startup fails, inspect the retained process output and `d3k logs --type server`; do not launch a separate dev server. -1. Close any stale `agent-browser` daemon before launching with `--profile`. Otherwise `agent-browser` will reuse the existing daemon and print `--profile ignored`. - ```bash - d3k agent-browser close --all - ``` +## User-Driven Testing -2. Start the app through d3k in `servers-only` mode and keep that command running. In Codex, this is more reliable than asking d3k to launch the browser itself when a fresh profile is required. - ```bash - d3k --no-agent --no-skills --servers-only --command "npm run dev -- -H 127.0.0.1 -p 3000" --port 3000 --startup-timeout 90 --no-tui - ``` +When the user says "let me test": - Adjust the package-manager command and port for the project. Prefer `--command` over `--script` when passing framework flags. For npm scripts, put flags after `--`; otherwise tools like Next.js can interpret the port as a project directory. - -3. Verify the server before opening more browser windows: - ```bash - curl -I http://127.0.0.1:3000 - ``` - -4. Open the fresh profile as a separate browser step: - ```bash - d3k agent-browser --allow-new-browser --profile /tmp/d3k-fresh-profile --headed open http://127.0.0.1:3000 - ``` - -5. Sanity-check the opened page: - ```bash - d3k agent-browser get title - d3k agent-browser snapshot -i - d3k errors - ``` - -Practical rules: - -- Prefer `127.0.0.1` for this workflow. If `localhost` hangs or flips between IPv4/IPv6 behavior, do not keep retrying browser launches. -- If `curl -I` hangs, the server is wedged even if the port appears occupied; restart the d3k server process before opening a browser. -- In `servers-only` mode there is no d3k-managed browser. Use `--allow-new-browser` only for the explicit non-auth fresh-profile open step; do not use `d3k cdp-port`. -- In sandboxed agent environments, rerun local-network checks and `agent-browser` opens outside the sandbox when sandbox networking blocks access to `127.0.0.1`. - -## Debugging Commands - -Use these first before ad-hoc log scraping: +1. Confirm the status response includes the app URL and `"browserConnected": true`. +2. Tell the user the monitored browser is ready. +3. Keep the d3k process running and wait for the user to reproduce the behavior. +4. When they report that it happened, begin with: ```bash d3k errors --context d3k logs -n 200 -d3k logs --type browser -d3k logs --type server ``` -## Browser Interaction +Do not replace the headed browser with automation while the user is testing. + +## Agent-Driven Testing -Use the already-monitored d3k browser session instead of launching a separate automation browser. +Drive the exact browser d3k is monitoring: ```bash -d3k agent-browser --require-d3k-browser open http://localhost:3000 d3k agent-browser snapshot -i d3k agent-browser click @e2 -d3k agent-browser screenshot /tmp/d3k-current.png +d3k agent-browser fill @e3 "text" +d3k errors --context ``` -`d3k agent-browser` auto-connects to the active d3k session's browser. `--require-d3k-browser` fails instead of creating a new browser when no d3k-managed browser exists. Manual CDP attachment, `d3k agent-browser connect `, and `--allow-new-browser` are explicit opt-in paths for targeting or creating a different browser, not the default. +Use `--require-d3k-browser` when opening a URL so failure cannot silently create another browser: -## Browser Tool Choice +```bash +d3k agent-browser --require-d3k-browser open "" +``` + +After every reproduction or code change, replay the relevant interaction and check `d3k errors --context` again. -Use the browser tool that matches the task instead of treating them as interchangeable: +## Evidence Commands -- `agent-browser` - - Default choice. - - Best for generic web apps and for driving the exact headed browser session that d3k is already monitoring. - - Use it when you need `snapshot`, ref-based `click`, `fill`, or to reproduce what the user sees in the monitored tab. -- `next-browser` - - Next.js-specific tool. - - Best for React/Next introspection: `tree`, `errors`, `logs`, `routes`, `project`, PPR inspection, and related Next dev-server signals. - - It is not a drop-in replacement for `agent-browser`: no accessibility `snapshot`, no ref-based `click`, and no `fill`. - - It launches its own daemon/browser flow and does not use d3k's active browser session. +Prefer these over ad-hoc log scraping: -Practical rule: +```bash +d3k status --json +d3k errors --context +d3k logs -n 200 +d3k logs --type browser +d3k logs --type server +``` -- Need to drive the same browser d3k is monitoring: use `agent-browser`. -- Need Next.js component tree or Next-specific diagnostics: use `next-browser`. +Artifacts live under `~/.d3k//`, including `session.json`, logs, screenshots, and the Chrome profile. -Examples: +## Browser and Auth Safety -```bash -# Same monitored browser session -d3k agent-browser snapshot -i -d3k agent-browser click @e2 +d3k must own browser startup by default. Its per-project Chrome profile preserves login state, cookies, and local storage. -# Next.js-specific inspection -d3k next-browser open http://localhost:3000 -d3k next-browser tree -d3k next-browser errors -d3k next-browser logs -``` +For Google OAuth, Supabase auth, and other auth-sensitive flows, never substitute raw Chrome, Playwright, a browser MCP session, manual CDP attachment, or `agent-browser --profile`. Those paths use a different browser/profile and can trigger "This browser or app may not be secure." -## Artifacts to Read +If the managed browser is unavailable, stop or interrupt the retained d3k process and restart d3k cleanly. Do not work around it by creating another browser. -- `~/.d3k/{project}/d3k.log` -- `~/.d3k/{project}/logs/` -- `~/.d3k/{project}/screenshots/` -- `~/.d3k/{project}/session.json` +Use `--headless` only for CI or when explicitly requested. Use `--servers-only` only when browser monitoring is intentionally unwanted. ## Operating Rules -- Prefer headed mode for interactive debugging. -- Use `--headless` only for CI or when explicitly requested. -- Use `--servers-only` only when browser monitoring is intentionally disabled, and not for auth-sensitive debugging. +- Do not run `npm run dev`, `bun run dev`, or another dev server alongside d3k. +- Do not start a second d3k when `d3k status --json` reports an active one. +- Keep d3k alive across edits and retests. +- Preserve the project-stable Chrome profile unless the user explicitly asks for a fresh profile. +- Leave the runtime running when handing a headed browser to the user; stop it only when asked or when the task requires a clean restart. +- Portless routing is the default. Use `--no-portless` or `PORTLESS=0` only when direct localhost routing is explicitly required. diff --git a/plugins/dev3000/agent/skills/d3k/PUBLISH.md b/plugins/dev3000/agent/skills/d3k/PUBLISH.md index 254bb3dd..512c50ee 100644 --- a/plugins/dev3000/agent/skills/d3k/PUBLISH.md +++ b/plugins/dev3000/agent/skills/d3k/PUBLISH.md @@ -13,15 +13,15 @@ ### Short description -`Bootstraps d3k runtime for standalone AI apps` +`Agent-owned local web debugging with a managed browser` ### Long description -`Installs/initializes dev3000 (d3k) for standalone agent shells (Codex, Cursor, Claude Code), starts d3k as the default runtime, and uses unified logs plus CDP browser control instead of raw npm/bun dev.` +`Starts or reuses d3k in a retained background agent session, opens the project-stable managed browser, and uses unified browser/server evidence instead of raw dev servers or separate automation browsers.` ### Default prompt -`Use $d3k to initialize d3k, start the correct runtime, and drive debugging with unified logs and CDP browser controls.` +`Use $d3k to let me test this project in its monitored browser, then inspect the captured evidence after I reproduce the issue.` ## Source URL diff --git a/plugins/dev3000/agent/skills/d3k/SKILL.md b/plugins/dev3000/agent/skills/d3k/SKILL.md index 9f6260fc..511c3ec0 100644 --- a/plugins/dev3000/agent/skills/d3k/SKILL.md +++ b/plugins/dev3000/agent/skills/d3k/SKILL.md @@ -1,197 +1,126 @@ --- -description: "Bootstrap d3k in standalone AI apps (Codex, Cursor, Claude Code): detect/install dev3000, start d3k as the runtime, and use unified logs plus d3k-owned browser/session control instead of running npm/bun dev directly." +description: "Use when the user asks to use d3k, run/dev/test/debug a web project with d3k, or reproduce a browser issue. Own the runtime: reuse or background-start d3k non-interactively, wait for readiness, use its project-stable managed Chrome profile, and inspect unified browser/server evidence." --- -# d3k Standalone Bootstrap +# d3k Agent Runtime -Use this skill when working in a standalone AI app and you need reliable local web debugging with browser + server context. +d3k is the local web runtime for this task. It starts the dev server behind a stable Portless URL, owns a project-stable Chrome profile, and records server logs, browser console output, network activity, interactions, and screenshots in one timeline. -## Why d3k-first +When this skill triggers, operate d3k. Do not merely tell the user how to run it. -- `d3k` captures server logs, browser console, network events, and screenshots in one timeline. -- `d3k` owns the browser session so the agent can control the same browser being monitored. -- Running `npm run dev` or `bun run dev` directly omits this unified telemetry and usually leads to weaker diagnoses. +## Interpret the Request -## Auth-Sensitive Browser Rule +- "Let me test/dev my project with d3k": prepare the runtime and headed browser, confirm it is ready, then hand control to the user. Wait for them to reproduce the issue before inspecting evidence. +- "Test/debug/fix this with d3k": prepare the runtime, then drive the managed browser and investigate autonomously. +- If ambiguous, start the runtime and browser first. That action is safe and useful for either path. -For Google OAuth, Supabase auth, and any other auth-sensitive debugging, d3k must own browser startup. Start d3k normally so it launches the app and browser together, including `--app-url` when the target URL is known. +## Start or Reuse d3k -Do not use `d3k agent-browser --profile ... --headed open ...`, raw Chrome, Playwright, browser MCP sessions, manual CDP attachment, or any other separate automation browser for auth debugging unless the user explicitly asks for that path. Agent-browser-created/custom Chrome profiles can be rejected by Google with `This browser or app may not be secure`. +Run from the project root. + +1. Check for an existing project runtime: -After d3k has launched the browser, use the safe managed-browser path: ```bash -d3k agent-browser --require-d3k-browser open "" -d3k agent-browser snapshot -i -d3k agent-browser click @e1 -d3k errors --context +d3k status --json ``` -If this fails because no d3k-managed browser exists, restart d3k cleanly with its normal browser-owning flow. Do not fall back to creating a new agent-browser Chrome for auth. +If it reports `"running": true`, reuse it. Do not start a second dev server or browser. -## Bootstrap Workflow - -1. Confirm whether `d3k` is installed: -```bash -command -v d3k >/dev/null && d3k --version -``` +2. If d3k is not installed, install it: -2. If `d3k` is missing, install dev3000 globally (prefer Bun): ```bash bun install -g dev3000 ``` -Fallback if Bun is unavailable: -```bash -npm install -g dev3000 -``` -3. Start d3k as the runtime and let d3k own browser startup. When the app command, port, and target URL are known, use the normal app-debugging shape: -```bash -d3k --no-agent --command "" --port --startup-timeout --no-tui --app-url "" -``` +Use `npm install -g dev3000` only when Bun is unavailable. - For a repo-default shell with no target URL yet: -```bash -d3k --no-agent --no-tui -t -``` - -4. Keep d3k running while editing code. Do not start a second dev server with `npm/bun dev`. +3. Start d3k with the agent's shell/process tool as a retained background or yielded session: -5. Drive the page through d3k's active browser session: ```bash -d3k agent-browser snapshot -i -d3k agent-browser click @e1 -d3k errors --context +d3k --no-agent --no-tui -t ``` -## Required Browser/Session Default - -When a user asks to start or debug an app with d3k, prefer d3k's normal browser-owning flow, including `--app-url` when a target URL is known. +Do not wait for this long-running command to exit. Keep its process/session handle so you can monitor or stop it later. Prefer the execution tool's background/session support over shelling with `&`. -Do not launch a separate raw Chrome, Playwright browser, browser MCP session, or manually attach to CDP unless the user explicitly asks for that path. Separate automation-only browser profiles can break OAuth flows, especially Google sign-in with `This browser or app may not be secure`. +If the target URL is already known, pass it so the managed browser opens there: -After d3k is running, drive the page through `d3k agent-browser ...` commands so interactions target d3k's active browser session. - -If profile or daemon state seems stale, first run: ```bash -d3k agent-browser close --all +d3k --no-agent --no-tui -t --app-url "" ``` -Then restart d3k cleanly with the normal browser-owning flow. +Let d3k auto-detect the package manager, dev command, and port. Add `--command`, `--script`, or `--port` only when detection is wrong or the user specified them. + +4. Poll until the runtime is ready: -For a normal app-debugging session, use: ```bash -d3k --no-agent --command "" --port --startup-timeout --no-tui --app-url "" -d3k agent-browser --require-d3k-browser open "" -d3k agent-browser snapshot -i -d3k agent-browser click @e1 -d3k errors --context +d3k status --json ``` -## Non-Auth Fresh Browser/Profile Startup - -Use this special-case workflow only for non-auth debugging when the user explicitly asks Codex to start d3k with a fresh browser/profile. Do not use this workflow for Google OAuth, Supabase auth, or any sign-in flow that may reject automation browsers. The default app-debugging workflow is to let d3k own browser startup and then interact through `d3k agent-browser`. +A successful status response is the readiness boundary. Prefer the reported Portless `appUrl`; the underlying app port may change between runs. If startup fails, inspect the retained process output and `d3k logs --type server`; do not launch a separate dev server. -1. Close any stale `agent-browser` daemon before launching with `--profile`. Otherwise `agent-browser` will reuse the existing daemon and print `--profile ignored`. - ```bash - d3k agent-browser close --all - ``` +## User-Driven Testing -2. Start the app through d3k in `servers-only` mode and keep that command running. In Codex, this is more reliable than asking d3k to launch the browser itself when a fresh profile is required. - ```bash - d3k --no-agent --no-skills --servers-only --command "npm run dev -- -H 127.0.0.1 -p 3000" --port 3000 --startup-timeout 90 --no-tui - ``` +When the user says "let me test": - Adjust the package-manager command and port for the project. Prefer `--command` over `--script` when passing framework flags. For npm scripts, put flags after `--`; otherwise tools like Next.js can interpret the port as a project directory. - -3. Verify the server before opening more browser windows: - ```bash - curl -I http://127.0.0.1:3000 - ``` - -4. Open the fresh profile as a separate browser step: - ```bash - d3k agent-browser --allow-new-browser --profile /tmp/d3k-fresh-profile --headed open http://127.0.0.1:3000 - ``` - -5. Sanity-check the opened page: - ```bash - d3k agent-browser get title - d3k agent-browser snapshot -i - d3k errors - ``` - -Practical rules: - -- Prefer `127.0.0.1` for this workflow. If `localhost` hangs or flips between IPv4/IPv6 behavior, do not keep retrying browser launches. -- If `curl -I` hangs, the server is wedged even if the port appears occupied; restart the d3k server process before opening a browser. -- In `servers-only` mode there is no d3k-managed browser. Use `--allow-new-browser` only for the explicit non-auth fresh-profile open step; do not use `d3k cdp-port`. -- In sandboxed agent environments, rerun local-network checks and `agent-browser` opens outside the sandbox when sandbox networking blocks access to `127.0.0.1`. - -## Debugging Commands - -Use these first before ad-hoc log scraping: +1. Confirm the status response includes the app URL and `"browserConnected": true`. +2. Tell the user the monitored browser is ready. +3. Keep the d3k process running and wait for the user to reproduce the behavior. +4. When they report that it happened, begin with: ```bash d3k errors --context d3k logs -n 200 -d3k logs --type browser -d3k logs --type server ``` -## Browser Interaction +Do not replace the headed browser with automation while the user is testing. + +## Agent-Driven Testing -Use the already-monitored d3k browser session instead of launching a separate automation browser. +Drive the exact browser d3k is monitoring: ```bash -d3k agent-browser --require-d3k-browser open http://localhost:3000 d3k agent-browser snapshot -i d3k agent-browser click @e2 -d3k agent-browser screenshot /tmp/d3k-current.png +d3k agent-browser fill @e3 "text" +d3k errors --context ``` -`d3k agent-browser` auto-connects to the active d3k session's browser. `--require-d3k-browser` fails instead of creating a new browser when no d3k-managed browser exists. Manual CDP attachment, `d3k agent-browser connect `, and `--allow-new-browser` are explicit opt-in paths for targeting or creating a different browser, not the default. +Use `--require-d3k-browser` when opening a URL so failure cannot silently create another browser: -## Browser Tool Choice +```bash +d3k agent-browser --require-d3k-browser open "" +``` + +After every reproduction or code change, replay the relevant interaction and check `d3k errors --context` again. -Use the browser tool that matches the task instead of treating them as interchangeable: +## Evidence Commands -- `agent-browser` - - Default choice. - - Best for generic web apps and for driving the exact headed browser session that d3k is already monitoring. - - Use it when you need `snapshot`, ref-based `click`, `fill`, or to reproduce what the user sees in the monitored tab. -- `next-browser` - - Next.js-specific tool. - - Best for React/Next introspection: `tree`, `errors`, `logs`, `routes`, `project`, PPR inspection, and related Next dev-server signals. - - It is not a drop-in replacement for `agent-browser`: no accessibility `snapshot`, no ref-based `click`, and no `fill`. - - It launches its own daemon/browser flow and does not use d3k's active browser session. +Prefer these over ad-hoc log scraping: -Practical rule: +```bash +d3k status --json +d3k errors --context +d3k logs -n 200 +d3k logs --type browser +d3k logs --type server +``` -- Need to drive the same browser d3k is monitoring: use `agent-browser`. -- Need Next.js component tree or Next-specific diagnostics: use `next-browser`. +Artifacts live under `~/.d3k//`, including `session.json`, logs, screenshots, and the Chrome profile. -Examples: +## Browser and Auth Safety -```bash -# Same monitored browser session -d3k agent-browser snapshot -i -d3k agent-browser click @e2 +d3k must own browser startup by default. Its per-project Chrome profile preserves login state, cookies, and local storage. -# Next.js-specific inspection -d3k next-browser open http://localhost:3000 -d3k next-browser tree -d3k next-browser errors -d3k next-browser logs -``` +For Google OAuth, Supabase auth, and other auth-sensitive flows, never substitute raw Chrome, Playwright, a browser MCP session, manual CDP attachment, or `agent-browser --profile`. Those paths use a different browser/profile and can trigger "This browser or app may not be secure." -## Artifacts to Read +If the managed browser is unavailable, stop or interrupt the retained d3k process and restart d3k cleanly. Do not work around it by creating another browser. -- `~/.d3k/{project}/d3k.log` -- `~/.d3k/{project}/logs/` -- `~/.d3k/{project}/screenshots/` -- `~/.d3k/{project}/session.json` +Use `--headless` only for CI or when explicitly requested. Use `--servers-only` only when browser monitoring is intentionally unwanted. ## Operating Rules -- Prefer headed mode for interactive debugging. -- Use `--headless` only for CI or when explicitly requested. -- Use `--servers-only` only when browser monitoring is intentionally disabled, and not for auth-sensitive debugging. +- Do not run `npm run dev`, `bun run dev`, or another dev server alongside d3k. +- Do not start a second d3k when `d3k status --json` reports an active one. +- Keep d3k alive across edits and retests. +- Preserve the project-stable Chrome profile unless the user explicitly asks for a fresh profile. +- Leave the runtime running when handing a headed browser to the user; stop it only when asked or when the task requires a clean restart. +- Portless routing is the default. Use `--no-portless` or `PORTLESS=0` only when direct localhost routing is explicitly required. diff --git a/plugins/dev3000/skills-lock.json b/plugins/dev3000/skills-lock.json index dbcced14..f7d3f718 100644 --- a/plugins/dev3000/skills-lock.json +++ b/plugins/dev3000/skills-lock.json @@ -5,7 +5,7 @@ "source": "vercel-labs/dev3000", "sourceType": "github", "skillPath": "skills/d3k/SKILL.md", - "computedHash": "13d37628042043672f4331264634ac19956f6e1fc9d97e320af12be5243343aa" + "computedHash": "6462ff3b28aaec3ba02f8419e4a6faace3ccb69165bf0da8f55a7da93393e2d3" } } } diff --git a/plugins/docus/.claude/skills/create-docs b/plugins/docus/.claude/skills/create-docs new file mode 120000 index 00000000..dac07543 --- /dev/null +++ b/plugins/docus/.claude/skills/create-docs @@ -0,0 +1 @@ +../../.agents/skills/create-docs \ No newline at end of file diff --git a/plugins/docus/.claude/skills/review-docs b/plugins/docus/.claude/skills/review-docs new file mode 120000 index 00000000..8d8fe1a9 --- /dev/null +++ b/plugins/docus/.claude/skills/review-docs @@ -0,0 +1 @@ +../../.agents/skills/review-docs \ No newline at end of file diff --git a/plugins/git-ai/.claude/skills/ask b/plugins/git-ai/.claude/skills/ask new file mode 120000 index 00000000..2db68559 --- /dev/null +++ b/plugins/git-ai/.claude/skills/ask @@ -0,0 +1 @@ +../../.agents/skills/ask \ No newline at end of file diff --git a/plugins/git-ai/.claude/skills/git-ai-search b/plugins/git-ai/.claude/skills/git-ai-search new file mode 120000 index 00000000..1635d65a --- /dev/null +++ b/plugins/git-ai/.claude/skills/git-ai-search @@ -0,0 +1 @@ +../../.agents/skills/git-ai-search \ No newline at end of file diff --git a/plugins/git-ai/.claude/skills/prompt-analysis b/plugins/git-ai/.claude/skills/prompt-analysis new file mode 120000 index 00000000..3b8bc272 --- /dev/null +++ b/plugins/git-ai/.claude/skills/prompt-analysis @@ -0,0 +1 @@ +../../.agents/skills/prompt-analysis \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-admin-reports b/plugins/google-workspace/.claude/skills/gws-admin-reports new file mode 120000 index 00000000..a0cbdf73 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-admin-reports @@ -0,0 +1 @@ +../../.agents/skills/gws-admin-reports \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-calendar b/plugins/google-workspace/.claude/skills/gws-calendar new file mode 120000 index 00000000..3dbc8165 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-calendar @@ -0,0 +1 @@ +../../.agents/skills/gws-calendar \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-calendar-agenda b/plugins/google-workspace/.claude/skills/gws-calendar-agenda new file mode 120000 index 00000000..5309b10e --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-calendar-agenda @@ -0,0 +1 @@ +../../.agents/skills/gws-calendar-agenda \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-calendar-insert b/plugins/google-workspace/.claude/skills/gws-calendar-insert new file mode 120000 index 00000000..9c87ae80 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-calendar-insert @@ -0,0 +1 @@ +../../.agents/skills/gws-calendar-insert \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-chat b/plugins/google-workspace/.claude/skills/gws-chat new file mode 120000 index 00000000..706de078 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-chat @@ -0,0 +1 @@ +../../.agents/skills/gws-chat \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-chat-send b/plugins/google-workspace/.claude/skills/gws-chat-send new file mode 120000 index 00000000..8d31876c --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-chat-send @@ -0,0 +1 @@ +../../.agents/skills/gws-chat-send \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-classroom b/plugins/google-workspace/.claude/skills/gws-classroom new file mode 120000 index 00000000..b9a8293a --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-classroom @@ -0,0 +1 @@ +../../.agents/skills/gws-classroom \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-docs b/plugins/google-workspace/.claude/skills/gws-docs new file mode 120000 index 00000000..7e0b2bd7 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-docs @@ -0,0 +1 @@ +../../.agents/skills/gws-docs \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-docs-write b/plugins/google-workspace/.claude/skills/gws-docs-write new file mode 120000 index 00000000..9ec51409 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-docs-write @@ -0,0 +1 @@ +../../.agents/skills/gws-docs-write \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-drive b/plugins/google-workspace/.claude/skills/gws-drive new file mode 120000 index 00000000..ed118107 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-drive @@ -0,0 +1 @@ +../../.agents/skills/gws-drive \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-drive-upload b/plugins/google-workspace/.claude/skills/gws-drive-upload new file mode 120000 index 00000000..7b3256cc --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-drive-upload @@ -0,0 +1 @@ +../../.agents/skills/gws-drive-upload \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-events b/plugins/google-workspace/.claude/skills/gws-events new file mode 120000 index 00000000..849df899 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-events @@ -0,0 +1 @@ +../../.agents/skills/gws-events \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-events-renew b/plugins/google-workspace/.claude/skills/gws-events-renew new file mode 120000 index 00000000..cbaff727 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-events-renew @@ -0,0 +1 @@ +../../.agents/skills/gws-events-renew \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-events-subscribe b/plugins/google-workspace/.claude/skills/gws-events-subscribe new file mode 120000 index 00000000..bb8df6e1 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-events-subscribe @@ -0,0 +1 @@ +../../.agents/skills/gws-events-subscribe \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-forms b/plugins/google-workspace/.claude/skills/gws-forms new file mode 120000 index 00000000..cbaf837f --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-forms @@ -0,0 +1 @@ +../../.agents/skills/gws-forms \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-gmail b/plugins/google-workspace/.claude/skills/gws-gmail new file mode 120000 index 00000000..539fd358 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-gmail @@ -0,0 +1 @@ +../../.agents/skills/gws-gmail \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-gmail-forward b/plugins/google-workspace/.claude/skills/gws-gmail-forward new file mode 120000 index 00000000..cfbf67c3 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-gmail-forward @@ -0,0 +1 @@ +../../.agents/skills/gws-gmail-forward \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-gmail-read b/plugins/google-workspace/.claude/skills/gws-gmail-read new file mode 120000 index 00000000..c5b6c147 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-gmail-read @@ -0,0 +1 @@ +../../.agents/skills/gws-gmail-read \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-gmail-reply b/plugins/google-workspace/.claude/skills/gws-gmail-reply new file mode 120000 index 00000000..bf46a8b6 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-gmail-reply @@ -0,0 +1 @@ +../../.agents/skills/gws-gmail-reply \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-gmail-reply-all b/plugins/google-workspace/.claude/skills/gws-gmail-reply-all new file mode 120000 index 00000000..a91ece4a --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-gmail-reply-all @@ -0,0 +1 @@ +../../.agents/skills/gws-gmail-reply-all \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-gmail-send b/plugins/google-workspace/.claude/skills/gws-gmail-send new file mode 120000 index 00000000..8cfd6b73 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-gmail-send @@ -0,0 +1 @@ +../../.agents/skills/gws-gmail-send \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-gmail-triage b/plugins/google-workspace/.claude/skills/gws-gmail-triage new file mode 120000 index 00000000..6275cd8d --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-gmail-triage @@ -0,0 +1 @@ +../../.agents/skills/gws-gmail-triage \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-gmail-watch b/plugins/google-workspace/.claude/skills/gws-gmail-watch new file mode 120000 index 00000000..42ff1bc9 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-gmail-watch @@ -0,0 +1 @@ +../../.agents/skills/gws-gmail-watch \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-keep b/plugins/google-workspace/.claude/skills/gws-keep new file mode 120000 index 00000000..9c985289 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-keep @@ -0,0 +1 @@ +../../.agents/skills/gws-keep \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-meet b/plugins/google-workspace/.claude/skills/gws-meet new file mode 120000 index 00000000..275f80c4 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-meet @@ -0,0 +1 @@ +../../.agents/skills/gws-meet \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-modelarmor b/plugins/google-workspace/.claude/skills/gws-modelarmor new file mode 120000 index 00000000..e0d87302 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-modelarmor @@ -0,0 +1 @@ +../../.agents/skills/gws-modelarmor \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-modelarmor-create-template b/plugins/google-workspace/.claude/skills/gws-modelarmor-create-template new file mode 120000 index 00000000..7beb6f26 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-modelarmor-create-template @@ -0,0 +1 @@ +../../.agents/skills/gws-modelarmor-create-template \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-modelarmor-sanitize-prompt b/plugins/google-workspace/.claude/skills/gws-modelarmor-sanitize-prompt new file mode 120000 index 00000000..2c2e075a --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-modelarmor-sanitize-prompt @@ -0,0 +1 @@ +../../.agents/skills/gws-modelarmor-sanitize-prompt \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-modelarmor-sanitize-response b/plugins/google-workspace/.claude/skills/gws-modelarmor-sanitize-response new file mode 120000 index 00000000..6ef3a328 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-modelarmor-sanitize-response @@ -0,0 +1 @@ +../../.agents/skills/gws-modelarmor-sanitize-response \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-people b/plugins/google-workspace/.claude/skills/gws-people new file mode 120000 index 00000000..1c1d6bef --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-people @@ -0,0 +1 @@ +../../.agents/skills/gws-people \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-script b/plugins/google-workspace/.claude/skills/gws-script new file mode 120000 index 00000000..571c9498 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-script @@ -0,0 +1 @@ +../../.agents/skills/gws-script \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-script-push b/plugins/google-workspace/.claude/skills/gws-script-push new file mode 120000 index 00000000..a690bf67 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-script-push @@ -0,0 +1 @@ +../../.agents/skills/gws-script-push \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-shared b/plugins/google-workspace/.claude/skills/gws-shared new file mode 120000 index 00000000..55b3ceda --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-shared @@ -0,0 +1 @@ +../../.agents/skills/gws-shared \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-sheets b/plugins/google-workspace/.claude/skills/gws-sheets new file mode 120000 index 00000000..89c52b60 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-sheets @@ -0,0 +1 @@ +../../.agents/skills/gws-sheets \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-sheets-append b/plugins/google-workspace/.claude/skills/gws-sheets-append new file mode 120000 index 00000000..63e73807 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-sheets-append @@ -0,0 +1 @@ +../../.agents/skills/gws-sheets-append \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-sheets-read b/plugins/google-workspace/.claude/skills/gws-sheets-read new file mode 120000 index 00000000..d652ba4b --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-sheets-read @@ -0,0 +1 @@ +../../.agents/skills/gws-sheets-read \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-slides b/plugins/google-workspace/.claude/skills/gws-slides new file mode 120000 index 00000000..6bdd6344 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-slides @@ -0,0 +1 @@ +../../.agents/skills/gws-slides \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-tasks b/plugins/google-workspace/.claude/skills/gws-tasks new file mode 120000 index 00000000..ce75ec88 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-tasks @@ -0,0 +1 @@ +../../.agents/skills/gws-tasks \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-workflow b/plugins/google-workspace/.claude/skills/gws-workflow new file mode 120000 index 00000000..44ee741e --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-workflow @@ -0,0 +1 @@ +../../.agents/skills/gws-workflow \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-workflow-email-to-task b/plugins/google-workspace/.claude/skills/gws-workflow-email-to-task new file mode 120000 index 00000000..a48618f9 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-workflow-email-to-task @@ -0,0 +1 @@ +../../.agents/skills/gws-workflow-email-to-task \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-workflow-file-announce b/plugins/google-workspace/.claude/skills/gws-workflow-file-announce new file mode 120000 index 00000000..7b32e249 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-workflow-file-announce @@ -0,0 +1 @@ +../../.agents/skills/gws-workflow-file-announce \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-workflow-meeting-prep b/plugins/google-workspace/.claude/skills/gws-workflow-meeting-prep new file mode 120000 index 00000000..e58774cd --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-workflow-meeting-prep @@ -0,0 +1 @@ +../../.agents/skills/gws-workflow-meeting-prep \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-workflow-standup-report b/plugins/google-workspace/.claude/skills/gws-workflow-standup-report new file mode 120000 index 00000000..89398327 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-workflow-standup-report @@ -0,0 +1 @@ +../../.agents/skills/gws-workflow-standup-report \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/gws-workflow-weekly-digest b/plugins/google-workspace/.claude/skills/gws-workflow-weekly-digest new file mode 120000 index 00000000..62bf9d0c --- /dev/null +++ b/plugins/google-workspace/.claude/skills/gws-workflow-weekly-digest @@ -0,0 +1 @@ +../../.agents/skills/gws-workflow-weekly-digest \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/persona-content-creator b/plugins/google-workspace/.claude/skills/persona-content-creator new file mode 120000 index 00000000..75d3653f --- /dev/null +++ b/plugins/google-workspace/.claude/skills/persona-content-creator @@ -0,0 +1 @@ +../../.agents/skills/persona-content-creator \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/persona-customer-support b/plugins/google-workspace/.claude/skills/persona-customer-support new file mode 120000 index 00000000..7046bd1e --- /dev/null +++ b/plugins/google-workspace/.claude/skills/persona-customer-support @@ -0,0 +1 @@ +../../.agents/skills/persona-customer-support \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/persona-event-coordinator b/plugins/google-workspace/.claude/skills/persona-event-coordinator new file mode 120000 index 00000000..796a032f --- /dev/null +++ b/plugins/google-workspace/.claude/skills/persona-event-coordinator @@ -0,0 +1 @@ +../../.agents/skills/persona-event-coordinator \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/persona-exec-assistant b/plugins/google-workspace/.claude/skills/persona-exec-assistant new file mode 120000 index 00000000..b6fa265f --- /dev/null +++ b/plugins/google-workspace/.claude/skills/persona-exec-assistant @@ -0,0 +1 @@ +../../.agents/skills/persona-exec-assistant \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/persona-hr-coordinator b/plugins/google-workspace/.claude/skills/persona-hr-coordinator new file mode 120000 index 00000000..67b4f8d1 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/persona-hr-coordinator @@ -0,0 +1 @@ +../../.agents/skills/persona-hr-coordinator \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/persona-it-admin b/plugins/google-workspace/.claude/skills/persona-it-admin new file mode 120000 index 00000000..7c227ef4 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/persona-it-admin @@ -0,0 +1 @@ +../../.agents/skills/persona-it-admin \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/persona-project-manager b/plugins/google-workspace/.claude/skills/persona-project-manager new file mode 120000 index 00000000..40533673 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/persona-project-manager @@ -0,0 +1 @@ +../../.agents/skills/persona-project-manager \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/persona-researcher b/plugins/google-workspace/.claude/skills/persona-researcher new file mode 120000 index 00000000..037318f2 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/persona-researcher @@ -0,0 +1 @@ +../../.agents/skills/persona-researcher \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/persona-sales-ops b/plugins/google-workspace/.claude/skills/persona-sales-ops new file mode 120000 index 00000000..dab84f62 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/persona-sales-ops @@ -0,0 +1 @@ +../../.agents/skills/persona-sales-ops \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/persona-team-lead b/plugins/google-workspace/.claude/skills/persona-team-lead new file mode 120000 index 00000000..ba21e938 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/persona-team-lead @@ -0,0 +1 @@ +../../.agents/skills/persona-team-lead \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-backup-sheet-as-csv b/plugins/google-workspace/.claude/skills/recipe-backup-sheet-as-csv new file mode 120000 index 00000000..d2cfc075 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-backup-sheet-as-csv @@ -0,0 +1 @@ +../../.agents/skills/recipe-backup-sheet-as-csv \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-batch-invite-to-event b/plugins/google-workspace/.claude/skills/recipe-batch-invite-to-event new file mode 120000 index 00000000..17a380bc --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-batch-invite-to-event @@ -0,0 +1 @@ +../../.agents/skills/recipe-batch-invite-to-event \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-block-focus-time b/plugins/google-workspace/.claude/skills/recipe-block-focus-time new file mode 120000 index 00000000..9e23d515 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-block-focus-time @@ -0,0 +1 @@ +../../.agents/skills/recipe-block-focus-time \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-bulk-download-folder b/plugins/google-workspace/.claude/skills/recipe-bulk-download-folder new file mode 120000 index 00000000..e046300e --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-bulk-download-folder @@ -0,0 +1 @@ +../../.agents/skills/recipe-bulk-download-folder \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-collect-form-responses b/plugins/google-workspace/.claude/skills/recipe-collect-form-responses new file mode 120000 index 00000000..cbe6307a --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-collect-form-responses @@ -0,0 +1 @@ +../../.agents/skills/recipe-collect-form-responses \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-compare-sheet-tabs b/plugins/google-workspace/.claude/skills/recipe-compare-sheet-tabs new file mode 120000 index 00000000..2d8dc0ad --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-compare-sheet-tabs @@ -0,0 +1 @@ +../../.agents/skills/recipe-compare-sheet-tabs \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-copy-sheet-for-new-month b/plugins/google-workspace/.claude/skills/recipe-copy-sheet-for-new-month new file mode 120000 index 00000000..41226253 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-copy-sheet-for-new-month @@ -0,0 +1 @@ +../../.agents/skills/recipe-copy-sheet-for-new-month \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-create-classroom-course b/plugins/google-workspace/.claude/skills/recipe-create-classroom-course new file mode 120000 index 00000000..5899da45 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-create-classroom-course @@ -0,0 +1 @@ +../../.agents/skills/recipe-create-classroom-course \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-create-doc-from-template b/plugins/google-workspace/.claude/skills/recipe-create-doc-from-template new file mode 120000 index 00000000..78d8afd7 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-create-doc-from-template @@ -0,0 +1 @@ +../../.agents/skills/recipe-create-doc-from-template \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-create-events-from-sheet b/plugins/google-workspace/.claude/skills/recipe-create-events-from-sheet new file mode 120000 index 00000000..b6422472 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-create-events-from-sheet @@ -0,0 +1 @@ +../../.agents/skills/recipe-create-events-from-sheet \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-create-expense-tracker b/plugins/google-workspace/.claude/skills/recipe-create-expense-tracker new file mode 120000 index 00000000..dc100069 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-create-expense-tracker @@ -0,0 +1 @@ +../../.agents/skills/recipe-create-expense-tracker \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-create-feedback-form b/plugins/google-workspace/.claude/skills/recipe-create-feedback-form new file mode 120000 index 00000000..0264f187 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-create-feedback-form @@ -0,0 +1 @@ +../../.agents/skills/recipe-create-feedback-form \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-create-gmail-filter b/plugins/google-workspace/.claude/skills/recipe-create-gmail-filter new file mode 120000 index 00000000..aae6809d --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-create-gmail-filter @@ -0,0 +1 @@ +../../.agents/skills/recipe-create-gmail-filter \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-create-meet-space b/plugins/google-workspace/.claude/skills/recipe-create-meet-space new file mode 120000 index 00000000..8118cff1 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-create-meet-space @@ -0,0 +1 @@ +../../.agents/skills/recipe-create-meet-space \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-create-presentation b/plugins/google-workspace/.claude/skills/recipe-create-presentation new file mode 120000 index 00000000..479e35ae --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-create-presentation @@ -0,0 +1 @@ +../../.agents/skills/recipe-create-presentation \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-create-shared-drive b/plugins/google-workspace/.claude/skills/recipe-create-shared-drive new file mode 120000 index 00000000..7e1aa709 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-create-shared-drive @@ -0,0 +1 @@ +../../.agents/skills/recipe-create-shared-drive \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-create-task-list b/plugins/google-workspace/.claude/skills/recipe-create-task-list new file mode 120000 index 00000000..1b5455cb --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-create-task-list @@ -0,0 +1 @@ +../../.agents/skills/recipe-create-task-list \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-create-vacation-responder b/plugins/google-workspace/.claude/skills/recipe-create-vacation-responder new file mode 120000 index 00000000..6f714712 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-create-vacation-responder @@ -0,0 +1 @@ +../../.agents/skills/recipe-create-vacation-responder \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-draft-email-from-doc b/plugins/google-workspace/.claude/skills/recipe-draft-email-from-doc new file mode 120000 index 00000000..18bcfce2 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-draft-email-from-doc @@ -0,0 +1 @@ +../../.agents/skills/recipe-draft-email-from-doc \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-email-drive-link b/plugins/google-workspace/.claude/skills/recipe-email-drive-link new file mode 120000 index 00000000..ee653c46 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-email-drive-link @@ -0,0 +1 @@ +../../.agents/skills/recipe-email-drive-link \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-find-free-time b/plugins/google-workspace/.claude/skills/recipe-find-free-time new file mode 120000 index 00000000..3e27bf1e --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-find-free-time @@ -0,0 +1 @@ +../../.agents/skills/recipe-find-free-time \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-find-large-files b/plugins/google-workspace/.claude/skills/recipe-find-large-files new file mode 120000 index 00000000..2a83c5d7 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-find-large-files @@ -0,0 +1 @@ +../../.agents/skills/recipe-find-large-files \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-forward-labeled-emails b/plugins/google-workspace/.claude/skills/recipe-forward-labeled-emails new file mode 120000 index 00000000..cb1302fb --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-forward-labeled-emails @@ -0,0 +1 @@ +../../.agents/skills/recipe-forward-labeled-emails \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-generate-report-from-sheet b/plugins/google-workspace/.claude/skills/recipe-generate-report-from-sheet new file mode 120000 index 00000000..475eab4e --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-generate-report-from-sheet @@ -0,0 +1 @@ +../../.agents/skills/recipe-generate-report-from-sheet \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-label-and-archive-emails b/plugins/google-workspace/.claude/skills/recipe-label-and-archive-emails new file mode 120000 index 00000000..628ba559 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-label-and-archive-emails @@ -0,0 +1 @@ +../../.agents/skills/recipe-label-and-archive-emails \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-log-deal-update b/plugins/google-workspace/.claude/skills/recipe-log-deal-update new file mode 120000 index 00000000..47f06c92 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-log-deal-update @@ -0,0 +1 @@ +../../.agents/skills/recipe-log-deal-update \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-organize-drive-folder b/plugins/google-workspace/.claude/skills/recipe-organize-drive-folder new file mode 120000 index 00000000..26628af1 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-organize-drive-folder @@ -0,0 +1 @@ +../../.agents/skills/recipe-organize-drive-folder \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-plan-weekly-schedule b/plugins/google-workspace/.claude/skills/recipe-plan-weekly-schedule new file mode 120000 index 00000000..37095348 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-plan-weekly-schedule @@ -0,0 +1 @@ +../../.agents/skills/recipe-plan-weekly-schedule \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-post-mortem-setup b/plugins/google-workspace/.claude/skills/recipe-post-mortem-setup new file mode 120000 index 00000000..661eb6f8 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-post-mortem-setup @@ -0,0 +1 @@ +../../.agents/skills/recipe-post-mortem-setup \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-reschedule-meeting b/plugins/google-workspace/.claude/skills/recipe-reschedule-meeting new file mode 120000 index 00000000..db48b554 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-reschedule-meeting @@ -0,0 +1 @@ +../../.agents/skills/recipe-reschedule-meeting \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-review-meet-participants b/plugins/google-workspace/.claude/skills/recipe-review-meet-participants new file mode 120000 index 00000000..61b3124c --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-review-meet-participants @@ -0,0 +1 @@ +../../.agents/skills/recipe-review-meet-participants \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-review-overdue-tasks b/plugins/google-workspace/.claude/skills/recipe-review-overdue-tasks new file mode 120000 index 00000000..dde6965c --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-review-overdue-tasks @@ -0,0 +1 @@ +../../.agents/skills/recipe-review-overdue-tasks \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-save-email-attachments b/plugins/google-workspace/.claude/skills/recipe-save-email-attachments new file mode 120000 index 00000000..72f9ba75 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-save-email-attachments @@ -0,0 +1 @@ +../../.agents/skills/recipe-save-email-attachments \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-save-email-to-doc b/plugins/google-workspace/.claude/skills/recipe-save-email-to-doc new file mode 120000 index 00000000..00568e8d --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-save-email-to-doc @@ -0,0 +1 @@ +../../.agents/skills/recipe-save-email-to-doc \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-schedule-recurring-event b/plugins/google-workspace/.claude/skills/recipe-schedule-recurring-event new file mode 120000 index 00000000..ea6ead0d --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-schedule-recurring-event @@ -0,0 +1 @@ +../../.agents/skills/recipe-schedule-recurring-event \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-send-team-announcement b/plugins/google-workspace/.claude/skills/recipe-send-team-announcement new file mode 120000 index 00000000..e59007b4 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-send-team-announcement @@ -0,0 +1 @@ +../../.agents/skills/recipe-send-team-announcement \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-share-doc-and-notify b/plugins/google-workspace/.claude/skills/recipe-share-doc-and-notify new file mode 120000 index 00000000..625ac380 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-share-doc-and-notify @@ -0,0 +1 @@ +../../.agents/skills/recipe-share-doc-and-notify \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-share-event-materials b/plugins/google-workspace/.claude/skills/recipe-share-event-materials new file mode 120000 index 00000000..696b79ad --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-share-event-materials @@ -0,0 +1 @@ +../../.agents/skills/recipe-share-event-materials \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-share-folder-with-team b/plugins/google-workspace/.claude/skills/recipe-share-folder-with-team new file mode 120000 index 00000000..d7691960 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-share-folder-with-team @@ -0,0 +1 @@ +../../.agents/skills/recipe-share-folder-with-team \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-sync-contacts-to-sheet b/plugins/google-workspace/.claude/skills/recipe-sync-contacts-to-sheet new file mode 120000 index 00000000..43f300f9 --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-sync-contacts-to-sheet @@ -0,0 +1 @@ +../../.agents/skills/recipe-sync-contacts-to-sheet \ No newline at end of file diff --git a/plugins/google-workspace/.claude/skills/recipe-watch-drive-changes b/plugins/google-workspace/.claude/skills/recipe-watch-drive-changes new file mode 120000 index 00000000..b3fa5bdb --- /dev/null +++ b/plugins/google-workspace/.claude/skills/recipe-watch-drive-changes @@ -0,0 +1 @@ +../../.agents/skills/recipe-watch-drive-changes \ No newline at end of file diff --git a/plugins/greptile/.claude/skills/check-pr b/plugins/greptile/.claude/skills/check-pr new file mode 120000 index 00000000..b5085f31 --- /dev/null +++ b/plugins/greptile/.claude/skills/check-pr @@ -0,0 +1 @@ +../../.agents/skills/check-pr \ No newline at end of file diff --git a/plugins/greptile/.claude/skills/cli-review b/plugins/greptile/.claude/skills/cli-review new file mode 120000 index 00000000..f7f9386b --- /dev/null +++ b/plugins/greptile/.claude/skills/cli-review @@ -0,0 +1 @@ +../../.agents/skills/cli-review \ No newline at end of file diff --git a/plugins/greptile/.claude/skills/greploop b/plugins/greptile/.claude/skills/greploop new file mode 120000 index 00000000..f7b25640 --- /dev/null +++ b/plugins/greptile/.claude/skills/greploop @@ -0,0 +1 @@ +../../.agents/skills/greploop \ No newline at end of file diff --git a/plugins/lavish/.agents/skills/lavish/SKILL.md b/plugins/lavish/.agents/skills/lavish/SKILL.md index fac4ab56..ce7502d5 100644 --- a/plugins/lavish/.agents/skills/lavish/SKILL.md +++ b/plugins/lavish/.agents/skills/lavish/SKILL.md @@ -15,6 +15,7 @@ Lavish Editor helps agents turn rich HTML artifacts into collaborative human rev You do not need lavish-axi installed globally - invoke it with `npx -y lavish-axi `. If lavish-axi output shows a follow-up command starting with `lavish-axi`, run it as `npx -y lavish-axi ...` instead. +In restricted subprocess sandboxes, CI, or agent harnesses where `npx -y` exits opaquely (for example with status 216), use an already-installed copy directly: `node "$(npm root)/lavish-axi/dist/cli.mjs" ` for a local install, `node "$(npm root -g)/lavish-axi/dist/cli.mjs" ` for a global install, or the bare `lavish-axi ` bin after installing once. ## Request @@ -32,8 +33,9 @@ Use lavish-axi when the user asks for a visual artifact, HTML explainer, interac 1. Create the HTML artifact (default location `.lavish/.html` in the working directory). 2. Run `npx -y lavish-axi ` to open or resume a review session in the browser. 3. Run `npx -y lavish-axi poll ` to long-poll for the user's annotations, queued prompts, and browser-reported `layout_warnings`. + On the first poll, prefer `--agent-reply ""` so the conversation panel opens with context. The poll stays silent until the user acts or the real browser reports fresh layout warnings - leave it running, never kill it. - If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost. + If the poll gets killed or times out anyway, just re-run it - queued feedback is never lost. 4. If poll returns `layout_warnings`, follow the returned `next_step`: fix and re-check fresh error-severity findings, but proceed with a note instead of looping when every current warning is persistent or low-severity. 5. Apply human feedback, then poll again with `--agent-reply ""` to reply in the browser and keep the loop going. 6. Run `npx -y lavish-axi end ` when the review is finished. @@ -51,7 +53,7 @@ Use lavish-axi when the user asks for a visual artifact, HTML explainer, interac Run `npx -y lavish-axi playbook ` for focused, detailed guidance on any of these. One artifact often combines several playbooks (for example a plan that includes a comparison and a diagram), so MUST open each matching playbook before writing HTML. -For flows, architecture, state, or sequence diagrams, do not hand-build boxes-and-arrows from div/flexbox; open the diagram playbook and use Mermaid unless SVG is needed for richly annotated nodes. +For flows, architecture, state, or sequence diagrams, do not hand-build boxes-and-arrows from div/flexbox; open the diagram playbook and use the theme-aware Mermaid snippet from `npx -y lavish-axi design` unless SVG is needed for richly annotated nodes. - `diagram` - Map relationships, flows, state, and architecture - `table` - Turn dense records into scan-friendly review surfaces @@ -66,11 +68,12 @@ For flows, architecture, state, or sequence diagrams, do not hand-build boxes-an - Run `npx -y lavish-axi ` to open or resume a Lavish Editor session. If the user explicitly ended the session from the browser, this refuses to reopen it and explains why instead of reopening uninvited - pass `--reopen` only when the user asks for further review or something important needs their visual attention - Unless the user specifies another location, create HTML artifacts in the current working directory under `.lavish/` - Lavish serves the html file through a local express.js server. If your html needs to reference other filesystem assets such as images, CSS, fonts, and local scripts, copy them into the same directory as the HTML file, then reference them with relative paths from that directory. Never prepend `/` to those asset paths - root paths won't work -- Run `npx -y lavish-axi poll ` to wait for user feedback or browser-reported layout_warnings. It long-polls and stays silent until the user sends feedback, ends the session, or the real browser reports fresh layout_warnings, so leave it running - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; if the poll says every current warning is persistent or low-severity, proceed with a note instead of looping. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost. When it reports the session ended, stop polling and do not reopen it uninvited - deliver remaining updates in this conversation instead +- Run `npx -y lavish-axi poll ` to wait for user feedback or browser-reported layout_warnings. It long-polls and stays silent until the user sends feedback, ends the session, or the real browser reports fresh layout_warnings, so leave it running - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; if the poll says every current warning is persistent or low-severity, proceed with a note instead of looping. If it gets killed or times out anyway, just re-run it - queued feedback is never lost. When it reports the session ended, stop polling and do not reopen it uninvited - deliver remaining updates in this conversation instead +- Rendered Mermaid diagrams in `.mermaid` containers become embedded, editable Excalidraw whiteboards in the browser (click a diagram to unlock editing; a Fullscreen action opens it over the whole viewport) - flowchart, sequence, class, ER, and state diagrams convert to editable shapes; other types embed as an image to draw on. Scenes autosave locally; when a reload detects a changed Mermaid source, the reviewer explicitly chooses to re-convert and discard saved edits or keep editing the saved scene. Standalone and exported copies still render plain Mermaid. Queue feedback adds a prompt to the Conversation panel; when the user sends it, poll returns a tag "whiteboard" prompt carrying a bounded edit summary plus local scenePath (.excalidraw JSON) and previewPath (PNG) files - read the summary first, open the files only when needed, then apply the edits by updating the Mermaid source in the artifact (never try to write the scene back) - Run `npx -y lavish-axi end ` to end a session as the agent - ending it this way still allows a plain reopen later. When the user ends it from the browser instead, a later `npx -y lavish-axi ` refuses to reopen it without `--reopen` - Run `npx -y lavish-axi export [--out ]` to write a portable copy of the artifact - one HTML file with its LOCAL assets inlined - so it opens with no Lavish server and no sibling files. Remote CDN/font references are left as links, so it needs network to render those. Users can also export from the browser chrome's overflow menu - Run `npx -y lavish-axi share [--password ] [--token ]` to publish the artifact on ht-ml.app (https://ht-ml.app), a third-party hosting service not part of Lavish, and get back a visitable URL. Shares are PUBLIC by default, so anyone with the link can open them. Pass --password to publish a PRIVATE password-protected page; viewers must supply the password to view. Local assets are inlined; remote refs load over the network. It returns the url plus a secret update_key for managing the page later. Use --token or LAVISH_AXI_HTML_APP_TOKEN only when you have an optional bearer token; it is never required. Users can also publish from the browser chrome's overflow menu - Run `npx -y lavish-axi stop` to shut down the background server (it also self-stops when idle or after the last session ends with nothing connected) - Run `npx -y lavish-axi playbook ` for focused artifact guidance. One artifact often combines several playbooks (for example a plan that includes a comparison and a diagram), so MUST open each matching playbook before writing HTML. -- Lavish does not auto-inject any design system - artifacts stay portable so they render identically when opened directly without lavish-axi running. Before writing any HTML, decide the design direction in this strict priority order, and only move to the next step when the current one truly yields nothing: (1) if the user asked for a specific look or named design system, use that; (2) otherwise you must first inspect the project the artifact is about - the subject or product whose content or UI it represents, which may differ from your current working directory - and match that project's design system: Tailwind or theme config, shared CSS variables or design tokens, component library, brand assets, or existing styled pages. If the artifact previews, proposes, or mocks a specific app's UI, render it in that app's own design system so it faithfully shows the product, even when you are running in a different repo; (3) only when both steps come up empty, use the Lavish-recommended Tailwind CSS browser runtime v4 + DaisyUI v5, available via CDN - run `npx -y lavish-axi design` for a content-to-playbook router, a copy-pasteable CDN snippet, a Mermaid CDN snippet/init for diagrams, and the DaisyUI component reference, and prefer the Tailwind/DaisyUI CDN snippet over hand-writing styles unless explicitly instructed otherwise by the user. When you deliver the artifact, state which of the three design sources you used and why. +- Lavish does not auto-inject any design system - artifacts stay portable so they render identically when opened directly without lavish-axi running. Before writing any HTML: Decide the design direction in this strict priority order, and only move to the next step when the current one truly yields nothing: (1) if the user asked for a specific look or named design system, use that; (2) otherwise you must first inspect the project the artifact is about - the subject or product whose content or UI it represents, which may differ from your current working directory - and match that project's design system: Tailwind or theme config, shared CSS variables or design tokens, component library, brand assets, or existing styled pages. If the artifact previews, proposes, or mocks a specific app's UI, render it in that app's own design system so it faithfully shows the product, even when you are running in a different repo; (3) only when both steps come up empty, use the Lavish-recommended Tailwind CSS browser runtime v4 + DaisyUI v5, available via CDN, and prefer that CDN snippet over hand-writing styles unless explicitly instructed otherwise by the user. Run `npx -y lavish-axi design` for a content-to-playbook router, a copy-pasteable CDN snippet, a Mermaid CDN snippet/init for diagrams, and the DaisyUI component reference. When you deliver the artifact, state which of the three design sources you used and why. - Use lavish-axi when the user asks for a visual artifact, HTML explainer, interactive prototype, review surface, product or technical plan, comparison, report, or browser-based feedback loop diff --git a/plugins/lavish/.claude/skills/lavish b/plugins/lavish/.claude/skills/lavish new file mode 120000 index 00000000..0fa00290 --- /dev/null +++ b/plugins/lavish/.claude/skills/lavish @@ -0,0 +1 @@ +../../.agents/skills/lavish \ No newline at end of file diff --git a/plugins/lavish/agent/skills/lavish/SKILL.md b/plugins/lavish/agent/skills/lavish/SKILL.md index 256f3495..3c7c94ca 100644 --- a/plugins/lavish/agent/skills/lavish/SKILL.md +++ b/plugins/lavish/agent/skills/lavish/SKILL.md @@ -7,6 +7,7 @@ Lavish Editor helps agents turn rich HTML artifacts into collaborative human rev You do not need lavish-axi installed globally - invoke it with `npx -y lavish-axi `. If lavish-axi output shows a follow-up command starting with `lavish-axi`, run it as `npx -y lavish-axi ...` instead. +In restricted subprocess sandboxes, CI, or agent harnesses where `npx -y` exits opaquely (for example with status 216), use an already-installed copy directly: `node "$(npm root)/lavish-axi/dist/cli.mjs" ` for a local install, `node "$(npm root -g)/lavish-axi/dist/cli.mjs" ` for a global install, or the bare `lavish-axi ` bin after installing once. ## Request @@ -24,8 +25,9 @@ Use lavish-axi when the user asks for a visual artifact, HTML explainer, interac 1. Create the HTML artifact (default location `.lavish/.html` in the working directory). 2. Run `npx -y lavish-axi ` to open or resume a review session in the browser. 3. Run `npx -y lavish-axi poll ` to long-poll for the user's annotations, queued prompts, and browser-reported `layout_warnings`. + On the first poll, prefer `--agent-reply ""` so the conversation panel opens with context. The poll stays silent until the user acts or the real browser reports fresh layout warnings - leave it running, never kill it. - If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost. + If the poll gets killed or times out anyway, just re-run it - queued feedback is never lost. 4. If poll returns `layout_warnings`, follow the returned `next_step`: fix and re-check fresh error-severity findings, but proceed with a note instead of looping when every current warning is persistent or low-severity. 5. Apply human feedback, then poll again with `--agent-reply ""` to reply in the browser and keep the loop going. 6. Run `npx -y lavish-axi end ` when the review is finished. @@ -43,7 +45,7 @@ Use lavish-axi when the user asks for a visual artifact, HTML explainer, interac Run `npx -y lavish-axi playbook ` for focused, detailed guidance on any of these. One artifact often combines several playbooks (for example a plan that includes a comparison and a diagram), so MUST open each matching playbook before writing HTML. -For flows, architecture, state, or sequence diagrams, do not hand-build boxes-and-arrows from div/flexbox; open the diagram playbook and use Mermaid unless SVG is needed for richly annotated nodes. +For flows, architecture, state, or sequence diagrams, do not hand-build boxes-and-arrows from div/flexbox; open the diagram playbook and use the theme-aware Mermaid snippet from `npx -y lavish-axi design` unless SVG is needed for richly annotated nodes. - `diagram` - Map relationships, flows, state, and architecture - `table` - Turn dense records into scan-friendly review surfaces @@ -58,11 +60,12 @@ For flows, architecture, state, or sequence diagrams, do not hand-build boxes-an - Run `npx -y lavish-axi ` to open or resume a Lavish Editor session. If the user explicitly ended the session from the browser, this refuses to reopen it and explains why instead of reopening uninvited - pass `--reopen` only when the user asks for further review or something important needs their visual attention - Unless the user specifies another location, create HTML artifacts in the current working directory under `.lavish/` - Lavish serves the html file through a local express.js server. If your html needs to reference other filesystem assets such as images, CSS, fonts, and local scripts, copy them into the same directory as the HTML file, then reference them with relative paths from that directory. Never prepend `/` to those asset paths - root paths won't work -- Run `npx -y lavish-axi poll ` to wait for user feedback or browser-reported layout_warnings. It long-polls and stays silent until the user sends feedback, ends the session, or the real browser reports fresh layout_warnings, so leave it running - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; if the poll says every current warning is persistent or low-severity, proceed with a note instead of looping. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost. When it reports the session ended, stop polling and do not reopen it uninvited - deliver remaining updates in this conversation instead +- Run `npx -y lavish-axi poll ` to wait for user feedback or browser-reported layout_warnings. It long-polls and stays silent until the user sends feedback, ends the session, or the real browser reports fresh layout_warnings, so leave it running - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; if the poll says every current warning is persistent or low-severity, proceed with a note instead of looping. If it gets killed or times out anyway, just re-run it - queued feedback is never lost. When it reports the session ended, stop polling and do not reopen it uninvited - deliver remaining updates in this conversation instead +- Rendered Mermaid diagrams in `.mermaid` containers become embedded, editable Excalidraw whiteboards in the browser (click a diagram to unlock editing; a Fullscreen action opens it over the whole viewport) - flowchart, sequence, class, ER, and state diagrams convert to editable shapes; other types embed as an image to draw on. Scenes autosave locally; when a reload detects a changed Mermaid source, the reviewer explicitly chooses to re-convert and discard saved edits or keep editing the saved scene. Standalone and exported copies still render plain Mermaid. Queue feedback adds a prompt to the Conversation panel; when the user sends it, poll returns a tag "whiteboard" prompt carrying a bounded edit summary plus local scenePath (.excalidraw JSON) and previewPath (PNG) files - read the summary first, open the files only when needed, then apply the edits by updating the Mermaid source in the artifact (never try to write the scene back) - Run `npx -y lavish-axi end ` to end a session as the agent - ending it this way still allows a plain reopen later. When the user ends it from the browser instead, a later `npx -y lavish-axi ` refuses to reopen it without `--reopen` - Run `npx -y lavish-axi export [--out ]` to write a portable copy of the artifact - one HTML file with its LOCAL assets inlined - so it opens with no Lavish server and no sibling files. Remote CDN/font references are left as links, so it needs network to render those. Users can also export from the browser chrome's overflow menu - Run `npx -y lavish-axi share [--password ] [--token ]` to publish the artifact on ht-ml.app (https://ht-ml.app), a third-party hosting service not part of Lavish, and get back a visitable URL. Shares are PUBLIC by default, so anyone with the link can open them. Pass --password to publish a PRIVATE password-protected page; viewers must supply the password to view. Local assets are inlined; remote refs load over the network. It returns the url plus a secret update_key for managing the page later. Use --token or LAVISH_AXI_HTML_APP_TOKEN only when you have an optional bearer token; it is never required. Users can also publish from the browser chrome's overflow menu - Run `npx -y lavish-axi stop` to shut down the background server (it also self-stops when idle or after the last session ends with nothing connected) - Run `npx -y lavish-axi playbook ` for focused artifact guidance. One artifact often combines several playbooks (for example a plan that includes a comparison and a diagram), so MUST open each matching playbook before writing HTML. -- Lavish does not auto-inject any design system - artifacts stay portable so they render identically when opened directly without lavish-axi running. Before writing any HTML, decide the design direction in this strict priority order, and only move to the next step when the current one truly yields nothing: (1) if the user asked for a specific look or named design system, use that; (2) otherwise you must first inspect the project the artifact is about - the subject or product whose content or UI it represents, which may differ from your current working directory - and match that project's design system: Tailwind or theme config, shared CSS variables or design tokens, component library, brand assets, or existing styled pages. If the artifact previews, proposes, or mocks a specific app's UI, render it in that app's own design system so it faithfully shows the product, even when you are running in a different repo; (3) only when both steps come up empty, use the Lavish-recommended Tailwind CSS browser runtime v4 + DaisyUI v5, available via CDN - run `npx -y lavish-axi design` for a content-to-playbook router, a copy-pasteable CDN snippet, a Mermaid CDN snippet/init for diagrams, and the DaisyUI component reference, and prefer the Tailwind/DaisyUI CDN snippet over hand-writing styles unless explicitly instructed otherwise by the user. When you deliver the artifact, state which of the three design sources you used and why. +- Lavish does not auto-inject any design system - artifacts stay portable so they render identically when opened directly without lavish-axi running. Before writing any HTML: Decide the design direction in this strict priority order, and only move to the next step when the current one truly yields nothing: (1) if the user asked for a specific look or named design system, use that; (2) otherwise you must first inspect the project the artifact is about - the subject or product whose content or UI it represents, which may differ from your current working directory - and match that project's design system: Tailwind or theme config, shared CSS variables or design tokens, component library, brand assets, or existing styled pages. If the artifact previews, proposes, or mocks a specific app's UI, render it in that app's own design system so it faithfully shows the product, even when you are running in a different repo; (3) only when both steps come up empty, use the Lavish-recommended Tailwind CSS browser runtime v4 + DaisyUI v5, available via CDN, and prefer that CDN snippet over hand-writing styles unless explicitly instructed otherwise by the user. Run `npx -y lavish-axi design` for a content-to-playbook router, a copy-pasteable CDN snippet, a Mermaid CDN snippet/init for diagrams, and the DaisyUI component reference. When you deliver the artifact, state which of the three design sources you used and why. - Use lavish-axi when the user asks for a visual artifact, HTML explainer, interactive prototype, review surface, product or technical plan, comparison, report, or browser-based feedback loop diff --git a/plugins/lavish/skills-lock.json b/plugins/lavish/skills-lock.json index 0f5b5295..5ec0c163 100644 --- a/plugins/lavish/skills-lock.json +++ b/plugins/lavish/skills-lock.json @@ -5,7 +5,7 @@ "source": "kunchenguid/lavish-axi", "sourceType": "github", "skillPath": "skills/lavish/SKILL.md", - "computedHash": "c3a0299d820bc3b30613d7740718b194511201319047fa9d3075e0ab1e596bde" + "computedHash": "bdca0069c8d4aa89ea3a3e3b1c394de36431f91ec96531e277c2f23f6d92d6b7" } } } diff --git a/plugins/mastra/.claude/skills/mastra b/plugins/mastra/.claude/skills/mastra new file mode 120000 index 00000000..2d4ffc16 --- /dev/null +++ b/plugins/mastra/.claude/skills/mastra @@ -0,0 +1 @@ +../../.agents/skills/mastra \ No newline at end of file diff --git a/plugins/nostics/.claude/skills/add-diagnostic b/plugins/nostics/.claude/skills/add-diagnostic new file mode 120000 index 00000000..232e310b --- /dev/null +++ b/plugins/nostics/.claude/skills/add-diagnostic @@ -0,0 +1 @@ +../../.agents/skills/add-diagnostic \ No newline at end of file diff --git a/plugins/nostics/.claude/skills/nostics b/plugins/nostics/.claude/skills/nostics new file mode 120000 index 00000000..9f76d555 --- /dev/null +++ b/plugins/nostics/.claude/skills/nostics @@ -0,0 +1 @@ +../../.agents/skills/nostics \ No newline at end of file diff --git a/plugins/nuxt-seo/.claude/skills/nuxt-seo b/plugins/nuxt-seo/.claude/skills/nuxt-seo new file mode 120000 index 00000000..fb7f6572 --- /dev/null +++ b/plugins/nuxt-seo/.claude/skills/nuxt-seo @@ -0,0 +1 @@ +../../.agents/skills/nuxt-seo \ No newline at end of file diff --git a/plugins/nuxt-ui/.claude/skills/nuxt-ui b/plugins/nuxt-ui/.claude/skills/nuxt-ui new file mode 120000 index 00000000..f7d99193 --- /dev/null +++ b/plugins/nuxt-ui/.claude/skills/nuxt-ui @@ -0,0 +1 @@ +../../.agents/skills/nuxt-ui \ No newline at end of file diff --git a/plugins/nuxt/.claude/skills/nuxt b/plugins/nuxt/.claude/skills/nuxt new file mode 120000 index 00000000..7bf43bbc --- /dev/null +++ b/plugins/nuxt/.claude/skills/nuxt @@ -0,0 +1 @@ +../../.agents/skills/nuxt \ No newline at end of file diff --git a/plugins/pinia/.claude/skills/pinia b/plugins/pinia/.claude/skills/pinia new file mode 120000 index 00000000..b29e1ea3 --- /dev/null +++ b/plugins/pinia/.claude/skills/pinia @@ -0,0 +1 @@ +../../.agents/skills/pinia \ No newline at end of file diff --git a/plugins/playwright-cli/.claude/skills/playwright-cli b/plugins/playwright-cli/.claude/skills/playwright-cli new file mode 120000 index 00000000..a5bb5229 --- /dev/null +++ b/plugins/playwright-cli/.claude/skills/playwright-cli @@ -0,0 +1 @@ +../../.agents/skills/playwright-cli \ No newline at end of file diff --git a/plugins/pnpm/.claude/skills/pnpm b/plugins/pnpm/.claude/skills/pnpm new file mode 120000 index 00000000..c3181c55 --- /dev/null +++ b/plugins/pnpm/.claude/skills/pnpm @@ -0,0 +1 @@ +../../.agents/skills/pnpm \ No newline at end of file diff --git a/plugins/portless/.claude/skills/portless b/plugins/portless/.claude/skills/portless new file mode 120000 index 00000000..240f102d --- /dev/null +++ b/plugins/portless/.claude/skills/portless @@ -0,0 +1 @@ +../../.agents/skills/portless \ No newline at end of file diff --git a/plugins/prisma/.agents/skills/prisma-database-setup/SKILL.md b/plugins/prisma/.agents/skills/prisma-database-setup/SKILL.md index 1de91a64..51643e74 100644 --- a/plugins/prisma/.agents/skills/prisma-database-setup/SKILL.md +++ b/plugins/prisma/.agents/skills/prisma-database-setup/SKILL.md @@ -170,7 +170,7 @@ generator client { } ``` -For MongoDB, stay on the latest Prisma 6.x line and keep the connection URL in `schema.prisma`. Do not move a MongoDB project to the Prisma 7 SQL adapter setup. +For MongoDB, stay on the latest Prisma 6.x line and keep the connection URL in `schema.prisma`. Do not move a MongoDB project to the Prisma 7 SQL adapter setup. If a MongoDB project asks about upgrading Prisma versions, route to the `prisma-mongodb-upgrade` skill (stay-on-v6 vs Prisma Next is the real decision; Prisma 7 is not an option). ## Rule Files diff --git a/plugins/prisma/.agents/skills/prisma-upgrade-v7/SKILL.md b/plugins/prisma/.agents/skills/prisma-upgrade-v7/SKILL.md index 1ac08b97..23d4956a 100644 --- a/plugins/prisma/.agents/skills/prisma-upgrade-v7/SKILL.md +++ b/plugins/prisma/.agents/skills/prisma-upgrade-v7/SKILL.md @@ -41,9 +41,15 @@ Reference this skill when: - `removed-features` - removed middleware, metrics, and legacy CLI behavior - `accelerate-users` - migration notes for Accelerate users +## Using MongoDB? This guide does not apply + +Prisma 7 has no MongoDB connector. Do not apply any step in this guide to a project with +`provider = "mongodb"` — see the `prisma-mongodb-upgrade` skill for the actual decision +(stay on v6 deliberately vs migrate to Prisma Next). + ## Important Notes -- **MongoDB projects should stay on Prisma 6.x** - do not migrate MongoDB apps to Prisma 7's SQL client path +- **MongoDB projects should stay on Prisma 6.x or migrate to Prisma Next** - do not migrate MongoDB apps to Prisma 7's SQL client path (see `prisma-mongodb-upgrade`) - **Node.js 20.19.0+** required - **TypeScript 5.4.0+** required - **Latest stable Prisma ORM version**: `7.6.0` diff --git a/plugins/prisma/.claude/skills/prisma-cli b/plugins/prisma/.claude/skills/prisma-cli new file mode 120000 index 00000000..bc38a65a --- /dev/null +++ b/plugins/prisma/.claude/skills/prisma-cli @@ -0,0 +1 @@ +../../.agents/skills/prisma-cli \ No newline at end of file diff --git a/plugins/prisma/.claude/skills/prisma-client-api b/plugins/prisma/.claude/skills/prisma-client-api new file mode 120000 index 00000000..25a84415 --- /dev/null +++ b/plugins/prisma/.claude/skills/prisma-client-api @@ -0,0 +1 @@ +../../.agents/skills/prisma-client-api \ No newline at end of file diff --git a/plugins/prisma/.claude/skills/prisma-database-setup b/plugins/prisma/.claude/skills/prisma-database-setup new file mode 120000 index 00000000..f188a790 --- /dev/null +++ b/plugins/prisma/.claude/skills/prisma-database-setup @@ -0,0 +1 @@ +../../.agents/skills/prisma-database-setup \ No newline at end of file diff --git a/plugins/prisma/.claude/skills/prisma-driver-adapter-implementation b/plugins/prisma/.claude/skills/prisma-driver-adapter-implementation new file mode 120000 index 00000000..c4bd907d --- /dev/null +++ b/plugins/prisma/.claude/skills/prisma-driver-adapter-implementation @@ -0,0 +1 @@ +../../.agents/skills/prisma-driver-adapter-implementation \ No newline at end of file diff --git a/plugins/prisma/.claude/skills/prisma-postgres b/plugins/prisma/.claude/skills/prisma-postgres new file mode 120000 index 00000000..e195028e --- /dev/null +++ b/plugins/prisma/.claude/skills/prisma-postgres @@ -0,0 +1 @@ +../../.agents/skills/prisma-postgres \ No newline at end of file diff --git a/plugins/prisma/.claude/skills/prisma-upgrade-v7 b/plugins/prisma/.claude/skills/prisma-upgrade-v7 new file mode 120000 index 00000000..95a4d863 --- /dev/null +++ b/plugins/prisma/.claude/skills/prisma-upgrade-v7 @@ -0,0 +1 @@ +../../.agents/skills/prisma-upgrade-v7 \ No newline at end of file diff --git a/plugins/prisma/agent/skills/prisma-database-setup/SKILL.md b/plugins/prisma/agent/skills/prisma-database-setup/SKILL.md index accd1c1c..56e92f46 100644 --- a/plugins/prisma/agent/skills/prisma-database-setup/SKILL.md +++ b/plugins/prisma/agent/skills/prisma-database-setup/SKILL.md @@ -166,7 +166,7 @@ generator client { } ``` -For MongoDB, stay on the latest Prisma 6.x line and keep the connection URL in `schema.prisma`. Do not move a MongoDB project to the Prisma 7 SQL adapter setup. +For MongoDB, stay on the latest Prisma 6.x line and keep the connection URL in `schema.prisma`. Do not move a MongoDB project to the Prisma 7 SQL adapter setup. If a MongoDB project asks about upgrading Prisma versions, route to the `prisma-mongodb-upgrade` skill (stay-on-v6 vs Prisma Next is the real decision; Prisma 7 is not an option). ## Rule Files diff --git a/plugins/prisma/agent/skills/prisma-upgrade-v7/SKILL.md b/plugins/prisma/agent/skills/prisma-upgrade-v7/SKILL.md index 047773ae..3bbc4096 100644 --- a/plugins/prisma/agent/skills/prisma-upgrade-v7/SKILL.md +++ b/plugins/prisma/agent/skills/prisma-upgrade-v7/SKILL.md @@ -37,9 +37,15 @@ Reference this skill when: - `removed-features` - removed middleware, metrics, and legacy CLI behavior - `accelerate-users` - migration notes for Accelerate users +## Using MongoDB? This guide does not apply + +Prisma 7 has no MongoDB connector. Do not apply any step in this guide to a project with +`provider = "mongodb"` — see the `prisma-mongodb-upgrade` skill for the actual decision +(stay on v6 deliberately vs migrate to Prisma Next). + ## Important Notes -- **MongoDB projects should stay on Prisma 6.x** - do not migrate MongoDB apps to Prisma 7's SQL client path +- **MongoDB projects should stay on Prisma 6.x or migrate to Prisma Next** - do not migrate MongoDB apps to Prisma 7's SQL client path (see `prisma-mongodb-upgrade`) - **Node.js 20.19.0+** required - **TypeScript 5.4.0+** required - **Latest stable Prisma ORM version**: `7.6.0` diff --git a/plugins/prisma/skills-lock.json b/plugins/prisma/skills-lock.json index 0396f7c1..4f905e92 100644 --- a/plugins/prisma/skills-lock.json +++ b/plugins/prisma/skills-lock.json @@ -17,7 +17,7 @@ "source": "prisma/skills", "sourceType": "github", "skillPath": "prisma-database-setup/SKILL.md", - "computedHash": "46d2dc2f9c968722376bbb6a1a1389c82bd7cceb5973ac1486c54890514b8959" + "computedHash": "6a991a2530a6da7131e3fc71c353e8278702d0146b8b6e1d2fdfc2d3d0a21bd2" }, "prisma-driver-adapter-implementation": { "source": "prisma/skills", @@ -35,7 +35,7 @@ "source": "prisma/skills", "sourceType": "github", "skillPath": "prisma-upgrade-v7/SKILL.md", - "computedHash": "2e4d54a4c7a0fc2476e81686718a765f5eb2cacfb2fb0822f3109c76bf4a9bb0" + "computedHash": "0e07dc2831f86cf27627df914db43d862b0fc75d72c1f9070434aab7940bad7c" } } } diff --git a/plugins/react-native/.claude/skills/vercel-react-native-skills b/plugins/react-native/.claude/skills/vercel-react-native-skills new file mode 120000 index 00000000..8c988434 --- /dev/null +++ b/plugins/react-native/.claude/skills/vercel-react-native-skills @@ -0,0 +1 @@ +../../.agents/skills/vercel-react-native-skills \ No newline at end of file diff --git a/plugins/react/.claude/skills/vercel-composition-patterns b/plugins/react/.claude/skills/vercel-composition-patterns new file mode 120000 index 00000000..55a19e8f --- /dev/null +++ b/plugins/react/.claude/skills/vercel-composition-patterns @@ -0,0 +1 @@ +../../.agents/skills/vercel-composition-patterns \ No newline at end of file diff --git a/plugins/react/.claude/skills/vercel-react-best-practices b/plugins/react/.claude/skills/vercel-react-best-practices new file mode 120000 index 00000000..e567923b --- /dev/null +++ b/plugins/react/.claude/skills/vercel-react-best-practices @@ -0,0 +1 @@ +../../.agents/skills/vercel-react-best-practices \ No newline at end of file diff --git a/plugins/react/.claude/skills/vercel-react-view-transitions b/plugins/react/.claude/skills/vercel-react-view-transitions new file mode 120000 index 00000000..78a32226 --- /dev/null +++ b/plugins/react/.claude/skills/vercel-react-view-transitions @@ -0,0 +1 @@ +../../.agents/skills/vercel-react-view-transitions \ No newline at end of file diff --git a/plugins/shadcn-ui/.claude/skills/migrate-radix-to-base b/plugins/shadcn-ui/.claude/skills/migrate-radix-to-base new file mode 120000 index 00000000..1ee49446 --- /dev/null +++ b/plugins/shadcn-ui/.claude/skills/migrate-radix-to-base @@ -0,0 +1 @@ +../../.agents/skills/migrate-radix-to-base \ No newline at end of file diff --git a/plugins/shadcn-ui/.claude/skills/shadcn b/plugins/shadcn-ui/.claude/skills/shadcn new file mode 120000 index 00000000..8d5af6f6 --- /dev/null +++ b/plugins/shadcn-ui/.claude/skills/shadcn @@ -0,0 +1 @@ +../../.agents/skills/shadcn \ No newline at end of file diff --git a/plugins/shadcn-ui/agent/skills/migrate-radix-to-base/SKILL.md b/plugins/shadcn-ui/agent/skills/migrate-radix-to-base/SKILL.md new file mode 100644 index 00000000..005e00b3 --- /dev/null +++ b/plugins/shadcn-ui/agent/skills/migrate-radix-to-base/SKILL.md @@ -0,0 +1,171 @@ +--- +description: "Migrates React projects and components from Radix UI to Base UI. Use when asked to migrate from radix, move to base-ui, convert radix primitives, or switch a shadcn project's base library. Handles single components (\"migrate accordion\") and whole projects." +--- +# Radix UI -> Base UI migration + +You migrate shadcn wrappers, hand-rolled radix compositions, and their +consumers to `@base-ui/react`, keeping the project buildable at every step. +Be precise; never guess a mapping. When a prop or part is not in these +reference files, check `node_modules/@base-ui/react/**/*.d.ts` before +transforming, and record gaps in the report. + +## Preflight (always) + +1. `npx shadcn@latest info --json` (or the project's runner): gives the + current base, STYLE (e.g. `radix-lyra`), tailwind version, aliases, + installed components, and package manager. Trust it over inference. +2. Detect the package manager (packageManager field / lockfile: + pnpm-lock.yaml, bun.lock, yarn.lock, package-lock.json) and use IT for + every install. Never leave a stale lockfile. +3. Require a clean git tree; work on a branch; one commit per component. +4. Baseline check BEFORE touching dependencies: run the project's + typecheck/build so pre-existing failures are never attributed to you. +5. Install `@base-ui/react` alongside radix. Radix packages are removed only + after the LAST component is migrated (both coexist fine). + +## Strategy: golden pair first, transformation engine second + +- **Golden pair via the CLI (preferred).** If the project is shadcn with a + known style (`radix-