mirror of
https://github.com/Canner/WrenAI.git
synced 2026-08-29 00:03:22 +08:00
feat(wren): serve agent skills and reference docs from the CLI (#2329)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
name: Skills Version Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "skills/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
version-parity:
|
||||
name: Check skills/versions.json parity
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Verify versions.json matches SKILL.md frontmatter
|
||||
run: bash skills/check-versions.sh
|
||||
@@ -31,9 +31,9 @@
|
||||
📺 HERO DEMO (place here)
|
||||
─────────────────────────
|
||||
Suggested: a 5–10 second silent loop showing:
|
||||
1. Terminal: `wren ask "who are our top 10 customers this quarter?"`
|
||||
2. Agent fetches context (memory + MDL) — visible reasoning trace
|
||||
3. Final SQL + result table
|
||||
1. Terminal: `wren skills get onboarding` (agent fetches the workflow guide from the CLI)
|
||||
2. Agent walks the user through setup, then writes SQL via `wren query` — visible reasoning trace
|
||||
3. Final result table
|
||||
Format: .gif (≤2 MB) or .mp4 (autoplay-muted).
|
||||
Save under /assets/wrenai-demo.gif and use the line below:
|
||||
|
||||
@@ -64,65 +64,71 @@ Agents are everywhere. Claude Code, Cursor, ChatGPT, Aider, LangChain pipelines,
|
||||
|
||||
## Quickstart
|
||||
|
||||
WrenAI is **agent-driven by design**: you install the skill bundle once, then let your AI coding agent (Claude Code, Openclaw, Hermes, Codex, etc.) drive the rest — Python deps, DB connection, project scaffold, and first query.
|
||||
WrenAI is **agent-driven by design**: install the CLI, install a one-file
|
||||
discovery stub for your AI client, then let your AI agent drive the rest.
|
||||
Workflow guides live inside the CLI itself and are served on demand, so
|
||||
content always matches the installed version.
|
||||
|
||||
### 1. Install the skill bundle
|
||||
|
||||
Skills are workflow guides that teach AI coding agents (Claude Code, Openclaw, Hermes, Codex, etc.) how to drive the Wren CLI for you.
|
||||
### 1. Install the CLI
|
||||
|
||||
```bash
|
||||
npx skills add Canner/WrenAI --skill '*'
|
||||
pip install wrenai # core (DuckDB included)
|
||||
pip install "wrenai[postgres,memory]" # add per-datasource and memory extras as needed
|
||||
```
|
||||
|
||||
Have multiple AI coding agents installed and want the skills available in all of them? Pass `--agent '*'`:
|
||||
### 2. Install the discovery stub for your AI client
|
||||
|
||||
```bash
|
||||
npx skills add Canner/WrenAI --skill '*' --agent '*'
|
||||
npx skills add Canner/WrenAI # auto-detects Claude Code, Cursor, Cline, Codex, …
|
||||
```
|
||||
|
||||
Or via the install script:
|
||||
The stub is ~50 lines. It teaches your agent to fetch workflow guides via
|
||||
`wren skills get <name>` and shaped prompts via
|
||||
`wren ask "<question>" --guided|--direct` — everything else lives in the CLI.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Canner/WrenAI/main/skills/install.sh | bash
|
||||
```
|
||||
### 3. Ask your agent to set things up
|
||||
|
||||
See the [Skills reference](https://docs.getwren.ai/oss/reference/skills) for the full list of skills installed and what each one does.
|
||||
Open your agent in a project directory and say something like:
|
||||
|
||||
### 2. Ask your agent to set things up
|
||||
> "Use Wren to set up my Postgres database."
|
||||
|
||||
Open your agent in a project directory and ask:
|
||||
The agent runs `wren skills get onboarding`, follows the guide step-by-step,
|
||||
checks your environment, creates a connection profile, scaffolds the project,
|
||||
and runs a first query.
|
||||
|
||||
Use the `/wren-onboarding` skill to install and set up Wren AI.
|
||||
### 4. (Optional) Enrich the project
|
||||
|
||||
The agent will check your environment, install `wrenai`, create a connection profile, scaffold the project, and run a first query — all in one flow.
|
||||
Once onboarding finishes, ask:
|
||||
|
||||
### 3. (Optional) Enrich the project
|
||||
> "Enrich my Wren project with the business context in `raw/`."
|
||||
|
||||
Once onboarding finishes, give your project the business context schemas can't carry:
|
||||
The agent runs `wren skills get enrich-context` and follows the guide in
|
||||
**grill** mode (one question at a time) or **auto-pilot** mode (agent reads
|
||||
`<project>/raw/` and proposes). Both modes write to MDL, instructions,
|
||||
queries, and memory — all reviewable, all Git-friendly.
|
||||
|
||||
Use the `/wren-enrich-context` skill in grill mode.
|
||||
### 5. Ask questions
|
||||
|
||||
Two modes: **grill** (one question at a time, you in the loop) or **auto-pilot** (agent reads `<project>/raw/` and proposes). Both modes write to MDL, instructions, queries, and memory — all reviewable, all Git-friendly.
|
||||
> "Who are our top 10 customers by sales this quarter?"
|
||||
|
||||
### 4. Ask questions
|
||||
Your agent fetches MDL context, recalls similar past queries, writes
|
||||
governed SQL, and executes via `wren query`.
|
||||
|
||||
```bash
|
||||
# Ask any question
|
||||
"who are our top 10 customers by sales this quarter?"
|
||||
```
|
||||
|
||||
Or just ask your agent in natural language — it uses the context layer to resolve schema, recall similar past queries, and write governed SQL.
|
||||
|
||||
**Want to try it without your own database?** Ask your agent to run `/wren-onboarding` with the bundled `jaffle_shop` sample dataset — same flow, but you'll be querying a real warehouse end-to-end in a couple of minutes.
|
||||
**Want to try it without your own database?** Ask your agent to use the
|
||||
bundled `jaffle_shop` sample dataset — same flow, querying a real warehouse
|
||||
end-to-end in a couple of minutes.
|
||||
|
||||
## Two beats: scaffold fast, enrich deep
|
||||
|
||||
```bash
|
||||
/wren-onboarding # Scaffold a Wren project from your DB (agent-driven)
|
||||
/wren-enrich-context # One skill, two modes: (Under development)
|
||||
# grill — one question at a time, you in the loop
|
||||
# auto-pilot — agent reads <project>/raw/ and proposes
|
||||
wren ask "..." # Query through the context layer
|
||||
# Day 1 — agent-driven
|
||||
wren skills get onboarding # workflow guide: set up project + first query
|
||||
wren skills get enrich-context # workflow guide: add business context (cubes, units, enums)
|
||||
|
||||
# Day-to-day
|
||||
wren query --sql '...' # query through the MDL semantic layer
|
||||
wren ask "<question>" --guided # wrap a question for a weaker agent
|
||||
wren ask "<question>" --direct # wrap a question for a stronger agent
|
||||
```
|
||||
|
||||
Fast at first. Deep when you need it. Always reviewable and Git-friendly.
|
||||
|
||||
@@ -81,7 +81,12 @@ Issues = "https://github.com/Canner/WrenAI/issues"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/wren"]
|
||||
artifacts = ["src/wren/templates/*.html"]
|
||||
artifacts = [
|
||||
"src/wren/templates/*.html",
|
||||
"src/wren/skills_content/**/*.md",
|
||||
"src/wren/skills_content/**/*.py",
|
||||
"src/wren/ask_templates/*.tmpl",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Prompt-shaping helpers for ``wren ask``.
|
||||
|
||||
``wren ask`` wraps a user's natural-language prompt in one of two bundled
|
||||
templates and prints the rendered result to stdout. It does not execute any
|
||||
query — it produces a prompt for an agent to consume.
|
||||
|
||||
Modes:
|
||||
- ``guided`` — prepends a strict task flow (for weaker LLMs).
|
||||
- ``direct`` — minimal wrapping (for stronger LLMs).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from importlib import resources
|
||||
|
||||
_TEMPLATES_DIR = "ask_templates"
|
||||
_USER_PROMPT_PLACEHOLDER = "<USER_PROMPT>"
|
||||
|
||||
MODES = ("guided", "direct")
|
||||
|
||||
|
||||
class UnknownAskModeError(ValueError):
|
||||
"""Raised when a mode other than ``guided`` / ``direct`` is requested."""
|
||||
|
||||
|
||||
def render(mode: str, user_prompt: str) -> str:
|
||||
"""Return the rendered ``mode`` template with ``user_prompt`` substituted."""
|
||||
if mode not in MODES:
|
||||
raise UnknownAskModeError(mode)
|
||||
tpl = (resources.files("wren") / _TEMPLATES_DIR / f"{mode}.md.tmpl").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
return tpl.replace(_USER_PROMPT_PLACEHOLDER, user_prompt)
|
||||
@@ -0,0 +1,42 @@
|
||||
"""``wren ask`` — wrap a user prompt for agent consumption.
|
||||
|
||||
Mode (``--guided`` or ``--direct``) must be chosen explicitly; there is no
|
||||
default. The reason: the two modes wrap prompts very differently, and a
|
||||
silent default-change would alter agent behavior across an upgrade.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typer
|
||||
|
||||
from wren import ask as _ask
|
||||
|
||||
|
||||
def ask(
|
||||
prompt: str = typer.Argument(
|
||||
..., help="The user's natural-language question to wrap."
|
||||
),
|
||||
guided: bool = typer.Option(
|
||||
False,
|
||||
"--guided",
|
||||
help="Wrap in a strict-flow guided prompt (for weaker LLMs).",
|
||||
),
|
||||
direct: bool = typer.Option(
|
||||
False,
|
||||
"--direct",
|
||||
help="Wrap in a minimal direct prompt (for stronger LLMs).",
|
||||
),
|
||||
) -> None:
|
||||
"""Wrap PROMPT into a processed prompt for an agent.
|
||||
|
||||
Choose exactly one of ``--guided`` or ``--direct``.
|
||||
"""
|
||||
if guided == direct:
|
||||
# both False or both True
|
||||
typer.echo(
|
||||
"Error: choose exactly one of --guided or --direct (no default).",
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(2)
|
||||
mode = "guided" if guided else "direct"
|
||||
typer.echo(_ask.render(mode, prompt))
|
||||
@@ -0,0 +1,4 @@
|
||||
You have access to Wren CLI for semantic SQL queries.
|
||||
Run `wren skills list` or `wren --help` to discover capabilities.
|
||||
|
||||
User question: <USER_PROMPT>
|
||||
@@ -0,0 +1,21 @@
|
||||
You are an agent helping a user with Wren CLI.
|
||||
Identify the task type, then follow the matching flow:
|
||||
|
||||
TASK TYPE A — data question:
|
||||
1. wren context show [--path <project>] # see MDL models
|
||||
2. wren memory recall --nl "<keywords>" # similar past queries (skip if no memory)
|
||||
3. write SQL using model names (not raw tables)
|
||||
4. wren dry-plan --sql '...' # validate non-trivial SQL
|
||||
5. wren query --sql '...' # execute
|
||||
6. answer in natural language
|
||||
|
||||
TASK TYPE B — explore / understand the project:
|
||||
1. wren skills list
|
||||
2. wren skills get <name> [--full]
|
||||
3. follow the markdown
|
||||
|
||||
Constraints:
|
||||
- use model names, never invent column names (verify via wren context show)
|
||||
- never ask for credentials in chat — they go through .env
|
||||
|
||||
User question: <USER_PROMPT>
|
||||
@@ -591,54 +591,21 @@ def version():
|
||||
|
||||
# ── Docs subcommand ───────────────────────────────────────────────────────
|
||||
|
||||
docs_app = typer.Typer(name="docs", help="Generate documentation for Wren Engine")
|
||||
|
||||
|
||||
@docs_app.command(name="connection-info")
|
||||
def docs_connection_info(
|
||||
datasource: Annotated[
|
||||
Optional[str],
|
||||
typer.Argument(help="Data source name (e.g. postgres, mysql). Omit for all."),
|
||||
] = None,
|
||||
format: Annotated[
|
||||
str,
|
||||
typer.Option("--format", "-f", help="Output format: md or json"),
|
||||
] = "md",
|
||||
envelope: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--envelope",
|
||||
help='Wrap JSON output in {"datasource": ..., "properties": ...} format.',
|
||||
),
|
||||
] = False,
|
||||
):
|
||||
"""Show connection info fields for each data source."""
|
||||
from wren.docs import generate_json_schema, generate_markdown # noqa: PLC0415
|
||||
|
||||
fmt = format.lower()
|
||||
try:
|
||||
if fmt == "md":
|
||||
typer.echo(generate_markdown(datasource))
|
||||
elif fmt == "json":
|
||||
typer.echo(generate_json_schema(datasource, envelope=envelope))
|
||||
else:
|
||||
typer.echo(
|
||||
f"Error: unsupported format '{format}'. Use md or json.", err=True
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
except ValueError as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
from wren.docs_cli import docs_app # noqa: E402, PLC0415
|
||||
|
||||
app.add_typer(docs_app)
|
||||
|
||||
from wren.ask_cli import ask as _ask_command # noqa: E402, PLC0415
|
||||
from wren.cube_cli import cube_app # noqa: E402, PLC0415
|
||||
from wren.skills_cli import skills_app # noqa: E402, PLC0415
|
||||
from wren.utils_cli import utils_app # noqa: E402, PLC0415
|
||||
|
||||
app.command(name="ask")(_ask_command)
|
||||
|
||||
app.add_typer(context_app)
|
||||
app.add_typer(cube_app)
|
||||
app.add_typer(utils_app)
|
||||
app.add_typer(skills_app)
|
||||
|
||||
try:
|
||||
import lancedb # noqa: PLC0415, F401
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""``wren docs`` — connection-info generation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, Optional
|
||||
|
||||
import typer
|
||||
|
||||
docs_app = typer.Typer(name="docs", help="Generate documentation for Wren Engine")
|
||||
|
||||
|
||||
@docs_app.command(name="connection-info")
|
||||
def docs_connection_info(
|
||||
datasource: Annotated[
|
||||
Optional[str],
|
||||
typer.Argument(help="Data source name (e.g. postgres, mysql). Omit for all."),
|
||||
] = None,
|
||||
format: Annotated[
|
||||
str,
|
||||
typer.Option("--format", "-f", help="Output format: md or json"),
|
||||
] = "md",
|
||||
envelope: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--envelope",
|
||||
help='Wrap JSON output in {"datasource": ..., "properties": ...} format.',
|
||||
),
|
||||
] = False,
|
||||
):
|
||||
"""Show connection info fields for each data source."""
|
||||
from wren.docs import generate_json_schema, generate_markdown # noqa: PLC0415
|
||||
|
||||
fmt = format.lower()
|
||||
try:
|
||||
if fmt == "md":
|
||||
typer.echo(generate_markdown(datasource))
|
||||
elif fmt == "json":
|
||||
typer.echo(generate_json_schema(datasource, envelope=envelope))
|
||||
else:
|
||||
typer.echo(
|
||||
f"Error: unsupported format '{format}'. Use md or json.", err=True
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
except ValueError as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
@@ -0,0 +1,66 @@
|
||||
"""``wren skills`` — serve bundled Wren agent skill guides."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from wren import skills_delivery
|
||||
|
||||
skills_app = typer.Typer(
|
||||
name="skills",
|
||||
help="Serve Wren agent skill guides (run `wren skills list`).",
|
||||
)
|
||||
|
||||
|
||||
@skills_app.command(name="list")
|
||||
def list_cmd() -> None:
|
||||
"""List the available skill guides."""
|
||||
skills = skills_delivery.list_skills()
|
||||
if not skills:
|
||||
typer.echo("No skills available.")
|
||||
return
|
||||
typer.echo("Available skills (run `wren skills get <name>`):")
|
||||
for skill in skills:
|
||||
typer.echo(f" {skill.name:16}{skill.summary}")
|
||||
extras = []
|
||||
if skill.references:
|
||||
extras.append("references: " + ", ".join(skill.references))
|
||||
if skill.scripts:
|
||||
extras.append("scripts: " + ", ".join(skill.scripts))
|
||||
if extras:
|
||||
typer.echo(f" {'':16}" + " ".join(extras))
|
||||
|
||||
|
||||
@skills_app.command()
|
||||
def get(
|
||||
name: str = typer.Argument(..., help="Skill name (see `wren skills list`)."),
|
||||
full: bool = typer.Option(
|
||||
False, "--full", help="Include the skill's reference docs."
|
||||
),
|
||||
script: Optional[str] = typer.Option(
|
||||
None, "--script", help="Print a bundled script instead of the guide."
|
||||
),
|
||||
) -> None:
|
||||
"""Print a skill's main guide (or a bundled script) to stdout."""
|
||||
try:
|
||||
if script is not None:
|
||||
content = skills_delivery.get_script(name, script)
|
||||
else:
|
||||
content = skills_delivery.get_skill(name, full=full)
|
||||
except skills_delivery.SkillNotFoundError:
|
||||
typer.echo(
|
||||
f"Error: unknown skill '{name}'. "
|
||||
"Run `wren skills list` for available names.",
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
except skills_delivery.ScriptNotFoundError:
|
||||
typer.echo(
|
||||
f"Error: skill '{name}' has no script '{script}'. "
|
||||
"Run `wren skills list` to see available scripts.",
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
typer.echo(content)
|
||||
+9
-7
@@ -1,14 +1,15 @@
|
||||
---
|
||||
name: wren-dlt-connector
|
||||
name: dlt-connector
|
||||
description: "Connect SaaS data (HubSpot, Stripe, Salesforce, GitHub, Slack, etc.) to Wren Engine for SQL analysis. Guides the user through the full flow: install dlt, pick a SaaS source, set up credentials, run the data pipeline into DuckDB, then auto-generate a Wren semantic project from the loaded data. Use this skill whenever the user mentions: connecting SaaS data, importing data from an API, dlt pipelines, loading HubSpot/Stripe/Salesforce/GitHub/Slack data, querying SaaS data with SQL, or setting up a new data source from a REST API. Also trigger when the user already has a dlt-produced DuckDB file and wants to create a Wren project from it."
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: wrenai
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# wren-dlt-connector
|
||||
|
||||
> Reference docs (`dlt_sources`) and the `introspect_dlt` script are bundled. Pull references with `wren skills get dlt-connector --full`; fetch a script with `wren skills get dlt-connector --script <name>`.
|
||||
|
||||
Connect SaaS data to Wren Engine for SQL analysis — from zero to a verified, queryable project in one conversation.
|
||||
|
||||
## Who this is for
|
||||
@@ -50,7 +51,7 @@ The `introspect_dlt.py` script does this automatically when wren SDK is installe
|
||||
|
||||
### Step 1: Pick the SaaS source
|
||||
|
||||
Ask the user which SaaS service they want to connect. Read `references/dlt_sources.md` for a list of popular verified sources and their auth requirements. If the source isn't listed, check whether dlt has a verified source for it by searching `dlthub.com/docs/dlt-ecosystem/verified-sources`.
|
||||
Ask the user which SaaS service they want to connect. Read `dlt_sources` for a list of popular verified sources and their auth requirements. If the source isn't listed, check whether dlt has a verified source for it by searching `dlthub.com/docs/dlt-ecosystem/verified-sources`.
|
||||
|
||||
### Step 2: Install dlt
|
||||
|
||||
@@ -65,7 +66,7 @@ Create a Python script that:
|
||||
2. Configures the pipeline with `destination='duckdb'` and a local file path
|
||||
3. Runs the pipeline with `pipeline.run(source)`
|
||||
|
||||
Here's the general pattern — adapt it per source (check `references/dlt_sources.md` for source-specific templates):
|
||||
Here's the general pattern — adapt it per source (check `dlt_sources` for source-specific templates):
|
||||
|
||||
```python
|
||||
import dlt
|
||||
@@ -76,7 +77,7 @@ pipeline = dlt.pipeline(
|
||||
dataset_name="<source>_data",
|
||||
)
|
||||
|
||||
# Source-specific: check references/dlt_sources.md for auth patterns
|
||||
# Source-specific: check the dlt_sources reference for auth patterns
|
||||
source = <source_function>(api_key=dlt.secrets.value)
|
||||
|
||||
info = pipeline.run(source)
|
||||
@@ -89,7 +90,7 @@ dlt reads credentials from environment variables or `.dlt/secrets.toml`. The sim
|
||||
|
||||
```bash
|
||||
# Set the credential as an environment variable
|
||||
# The exact variable name depends on the source — check references/dlt_sources.md
|
||||
# The exact variable name depends on the source — check the dlt_sources reference
|
||||
export SOURCES__<SOURCE>__API_KEY="the-actual-key"
|
||||
```
|
||||
|
||||
@@ -130,7 +131,8 @@ con.close()
|
||||
Run the introspection script to auto-generate a complete Wren project from the DuckDB file:
|
||||
|
||||
```bash
|
||||
python <path-to-this-skill>/scripts/introspect_dlt.py \
|
||||
# first fetch the script: wren skills get dlt-connector --script introspect_dlt > introspect_dlt.py
|
||||
python introspect_dlt.py \
|
||||
--duckdb-path <path-to-duckdb-file> \
|
||||
--output-dir <project-directory> \
|
||||
--project-name <name>
|
||||
+15
-10
@@ -50,8 +50,8 @@ _EXCLUDED_SCHEMAS = frozenset({"information_schema", "pg_catalog"})
|
||||
@dataclass
|
||||
class Column:
|
||||
name: str
|
||||
raw_type: str # original DuckDB type
|
||||
wren_type: str # normalized via parse_type
|
||||
raw_type: str # original DuckDB type
|
||||
wren_type: str # normalized via parse_type
|
||||
is_nullable: bool
|
||||
|
||||
|
||||
@@ -208,7 +208,10 @@ def detect_relationships(tables: list[Table]) -> list[Relationship]:
|
||||
|
||||
for i in range(len(parts) - 1, 0, -1):
|
||||
candidate = "__".join(parts[:i])
|
||||
if candidate in tables_by_schema.get(t.schema, set()) and candidate != t.name:
|
||||
if (
|
||||
candidate in tables_by_schema.get(t.schema, set())
|
||||
and candidate != t.name
|
||||
):
|
||||
parent_name = candidate
|
||||
child_suffix = "__".join(parts[i:])
|
||||
break
|
||||
@@ -264,7 +267,11 @@ def generate_project_files(
|
||||
name_counts[t.name] = name_counts.get(t.name, 0) + 1
|
||||
|
||||
def resolve_name(schema: str, table_name: str) -> str:
|
||||
return f"{schema}__{table_name}" if name_counts.get(table_name, 0) > 1 else table_name
|
||||
return (
|
||||
f"{schema}__{table_name}"
|
||||
if name_counts.get(table_name, 0) > 1
|
||||
else table_name
|
||||
)
|
||||
|
||||
# -- wren_project.yml --
|
||||
project_config = {
|
||||
@@ -376,9 +383,7 @@ def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate a Wren project from a dlt DuckDB file."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--duckdb-path", required=True, help="Path to the .duckdb file"
|
||||
)
|
||||
parser.add_argument("--duckdb-path", required=True, help="Path to the .duckdb file")
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
default=".",
|
||||
@@ -435,10 +440,10 @@ def main():
|
||||
write_project(files, output_dir, force=args.force)
|
||||
print(f"\nWren project written to {output_dir}/")
|
||||
print(f" {len(tables)} models, {len(relationships)} relationships")
|
||||
print(f"\nNext steps:")
|
||||
print("\nNext steps:")
|
||||
print(f" cd {output_dir}")
|
||||
print(f" wren context validate")
|
||||
print(f" wren context build")
|
||||
print(" wren context validate")
|
||||
print(" wren context build")
|
||||
|
||||
finally:
|
||||
con.close()
|
||||
+13
-53
@@ -1,33 +1,15 @@
|
||||
---
|
||||
name: wren-enrich-context
|
||||
name: enrich-context
|
||||
description: "Augment a Wren project with business context that DB schema cannot carry — enum value meanings, units (USD vs cents, ms vs sec), NULL semantics, magic sentinels (-1 = unknown), soft-delete default filters, business synonyms, time-grain / TZ conventions, cross-system identifiers, currency rules, canonical-table preferences, AND named aggregation metrics (ARR, churn, DAU, WAU, NRR) proposed as cubes. Runs in one of two modes selected at session start: `grill` (one question at a time, user-driven) or `auto-pilot` (agent infers and applies, escalates only on conflicts and high-blast-radius additions like new cubes / views / relationships). Reads everything under <project>/raw/ (PDFs, glossaries, handbooks, code, data dictionaries) and optionally samples low-cardinality columns from the live DB (grill mode), compares against the current MDL / cubes / instructions.md / queries.yml / memory pairs, then fills gaps via the ten-category gap catalog and the cube proposal flow. Confirmed findings are written back to the right sink. Use when: user says 'enrich context', 'augment my project', 'grill me on this project', 'auto-fill my context', 'agent doesn't understand our docs / enum values / units / null meanings', 'business context is missing', 'what does status=A mean', 'is this amount in USD or cents', 'we keep getting wrong aggregations', 'add cubes for ARR / DAU / churn', 'we have a handbook / glossary / data dictionary the agent should know'; or after generating an MDL and noticing the agent lacks business semantics."
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: wren-engine
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Wren Enrich Context — Fill the Business-Context Gap
|
||||
|
||||
This skill exists because most business context never lives in a DB schema — it lives in handbooks, glossaries, finance reports, support playbooks, code comments, Slack rules-of-thumb. The agent reads those raw artifacts, finds what's missing from the Wren project, and **either grills the user one question at a time (grill mode) or applies its best inferences directly and hands over an audit (auto-pilot mode)** before writing back. The output lands in three (or four) sinks each project already has — no new artifact, no new tooling.
|
||||
|
||||
## Version check
|
||||
|
||||
Silently fetch `https://raw.githubusercontent.com/Canner/WrenAI/main/skills/versions.json`.
|
||||
Compare the `wren-enrich-context` key with this skill's version (from the frontmatter above).
|
||||
If the remote version is newer, notify the user before proceeding:
|
||||
|
||||
> A newer version of the **wren-enrich-context** skill is available.
|
||||
> Update with:
|
||||
> ```bash
|
||||
> npx skills add Canner/WrenAI --skill wren-enrich-context
|
||||
> ```
|
||||
> The CLI auto-detects your installed agent. To target a specific one, add `--agent <name>` (e.g., `claude-code`, `cursor`, `windsurf`, `cline`).
|
||||
|
||||
Continue regardless of update status.
|
||||
|
||||
---
|
||||
|
||||
## Hard rules — READ FIRST
|
||||
|
||||
### Universal (apply to both modes)
|
||||
@@ -51,8 +33,6 @@ Continue regardless of update status.
|
||||
|
||||
Everything else: apply directly and log to the audit list.
|
||||
|
||||
---
|
||||
|
||||
## Step 0 — Mode selection (before anything else)
|
||||
|
||||
Before touching the project or reading any file, ask the user which mode to run in. Lock the choice for the whole session — **no mid-session switching**; the user re-runs to change.
|
||||
@@ -67,8 +47,6 @@ Before touching the project or reading any file, ask the user which mode to run
|
||||
|
||||
Remember the choice as `MODE = grill | autopilot` and use it to branch Steps 6 and 9.
|
||||
|
||||
---
|
||||
|
||||
## Preflight
|
||||
|
||||
### Step 1 — Choose the Wren project
|
||||
@@ -106,7 +84,7 @@ wren context show >/dev/null 2>&1 || {
|
||||
}
|
||||
```
|
||||
|
||||
If either check fails, stop and tell the user — suggest `wren-onboarding` if it's not a project, or `wren context validate` if the manifest is broken.
|
||||
If either check fails, stop and tell the user — suggest `wren skills get onboarding` if it's not a project, or `wren context validate` if the manifest is broken.
|
||||
|
||||
From this point on, **every command and file path in this skill is relative to the chosen project root**. Do not switch projects mid-session — if the user wants to work a different project, end this session and re-run.
|
||||
|
||||
@@ -137,8 +115,6 @@ If you just created it (the directory was empty or new):
|
||||
|
||||
Wait for the user to confirm before continuing.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Read everything
|
||||
|
||||
Read both sides — the raw material and the current Wren context — before forming any opinion.
|
||||
@@ -159,13 +135,11 @@ Read every file under `raw/`. Use whatever capability your agent has natively (t
|
||||
| (Memory) stored pairs | `wren memory list -n 200 --output json` |
|
||||
| (Memory) schema as text | `wren memory describe` |
|
||||
|
||||
The memory rows only matter when `MEMORY_AVAILABLE = true`. Reading cubes is essential before any Lane 3 metric proposal — see `references/cube_proposals.md` for the duplication guard.
|
||||
|
||||
---
|
||||
The memory rows only matter when `MEMORY_AVAILABLE = true`. Reading cubes is essential before any Lane 3 metric proposal — see `cube_proposals` for the duplication guard.
|
||||
|
||||
## Step 4.5 — Ground-truth probe (grill mode default; auto-pilot opt-out)
|
||||
|
||||
When raw is silent on a column's enum / unit / null / magic / time semantics, the catalog's column-local categories (#1, #2, #3, #5, #7 in `references/gap_catalog.md`) can often be settled directly by sampling distinct values from the live DB. Read `references/gap_catalog.md` before this step — its *Trigger* column tells you which columns are probe candidates.
|
||||
When raw is silent on a column's enum / unit / null / magic / time semantics, the catalog's column-local categories (#1, #2, #3, #5, #7 in `gap_catalog`) can often be settled directly by sampling distinct values from the live DB. Read `gap_catalog` before this step — its *Trigger* column tells you which columns are probe candidates.
|
||||
|
||||
**Default policy by mode:**
|
||||
|
||||
@@ -202,13 +176,11 @@ wren --sql "SELECT MIN(<col>) AS lo, MAX(<col>) AS hi FROM <model>" --output jso
|
||||
- Never probe a column that already has a matching `[tag]` line — Universal Rule 1.
|
||||
- Probe results stay in working memory; do not write them to disk.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Three gap-detection lanes (in your head, no artifact)
|
||||
|
||||
Hold all three lanes in working memory. Do not write a `gaps.yml`.
|
||||
|
||||
Before sweeping, load `references/gap_catalog.md` — the ten business-semantic categories the schema cannot carry. Each lane consumes the catalog differently: Lane 1 walks it as type-aware mechanical triggers, Lane 2 classifies each atomic raw claim into one of the 10 categories before routing, Lane 3 seeds inference prompts when a trigger fires but raw is silent.
|
||||
Before sweeping, load `gap_catalog` — the ten business-semantic categories the schema cannot carry. Each lane consumes the catalog differently: Lane 1 walks it as type-aware mechanical triggers, Lane 2 classifies each atomic raw claim into one of the 10 categories before routing, Lane 3 seeds inference prompts when a trigger fires but raw is silent.
|
||||
|
||||
### Lane 1 — Structural coverage (mechanical)
|
||||
|
||||
@@ -221,7 +193,7 @@ Scan the current MDL and check:
|
||||
- `instructions.md` is more than the scaffold default?
|
||||
- `queries.yml` has at least a few canonical pairs?
|
||||
|
||||
Plus, walk every column / model against `references/gap_catalog.md` triggers:
|
||||
Plus, walk every column / model against `gap_catalog` triggers:
|
||||
|
||||
- For each column matching catalog #1 / #2 / #3 / #5 / #7 triggers → is the corresponding `[tag]` line present in `properties.description`?
|
||||
- For each model with a soft-delete column (`deleted_at`, `is_active`, `archived_at`, etc.) → is there a `## Default filters` rule in `instructions.md` covering it (catalog #4)?
|
||||
@@ -246,16 +218,14 @@ For each raw file, internally extract 5–15 **atomic claims** — single statem
|
||||
|
||||
After reading raw and the current MDL, propose additions the user did **not** literally state in raw but that would clearly help the agent later. Examples:
|
||||
|
||||
- "I see `quarterly_churn` referenced five times in `finance.pdf`. No existing cube covers it. Want me to add `cubes/quarterly_churn/metadata.yml` with measure = `COUNT(*) FILTER (WHERE churned_at IS NOT NULL) / NULLIF(COUNT(*), 0)`?" — see `references/cube_proposals.md` for the YAML template and duplication guard.
|
||||
- "I see `quarterly_churn` referenced five times in `finance.pdf`. No existing cube covers it. Want me to add `cubes/quarterly_churn/metadata.yml` with measure = `COUNT(*) FILTER (WHERE churned_at IS NOT NULL) / NULLIF(COUNT(*), 0)`?" — see `cube_proposals` for the YAML template and duplication guard.
|
||||
- "Your support handbook keeps mentioning `core users` without defining it. Is this `users WHERE tier = 'premium'`? Want me to make a view?"
|
||||
- "The data dictionary says `events.payload` is JSON but the column has no description — let me draft one."
|
||||
|
||||
For any aggregation-shaped proposal (`SUM`, `COUNT`, `AVG`, "by month / by status / per customer" patterns), **default to a cube**. Run `wren cube list` + `wren cube describe` first to confirm no existing cube already covers the measure expression; if one does, skip the proposal and add a `queries.yml` example pointing at the existing cube instead. The full decision tree, naming rules, and validation flow live in `references/cube_proposals.md`.
|
||||
For any aggregation-shaped proposal (`SUM`, `COUNT`, `AVG`, "by month / by status / per customer" patterns), **default to a cube**. Run `wren cube list` + `wren cube describe` first to confirm no existing cube already covers the measure expression; if one does, skip the proposal and add a `queries.yml` example pointing at the existing cube instead. The full decision tree, naming rules, and validation flow live in `cube_proposals`.
|
||||
|
||||
**In grill mode, open every Lane 3 question with "I'm guessing — ".** In auto-pilot, tag the audit entry with `agent inference` so the user sees you extrapolated.
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Resolve gaps
|
||||
|
||||
Branch on the `MODE` locked in Step 0.
|
||||
@@ -300,16 +270,14 @@ For everything else (Lane 1 mechanical fixes, Lane 2 unambiguous new claims, Lan
|
||||
|
||||
Auto-pilot does not pause for confirmation on each item — the user reviews the full diff + audit list in Step 9. They are the reviewer, not the gatekeeper.
|
||||
|
||||
---
|
||||
|
||||
## Step 7 — Routing & writeback
|
||||
|
||||
Decide the sink as part of the proposal (Step 6.3 in grill mode; Step 6.2 in auto-pilot), so the user can correct routing in grill mode and audit it in auto-pilot.
|
||||
|
||||
| Finding type | Sink | How to write |
|
||||
|---|---|---|
|
||||
| Schema structure / relationship / view / model or column description | **MDL YAML** under `models/`, `views/`, `relationships.yml` | Edit the YAML file directly. For catalog #1 / #2 / #3 / #5 / #7 / PII, append a `[tag]` line to `properties.description` (prose first, then one tag per category). See `references/gap_catalog.md` for the exact tag format and triggers. |
|
||||
| Aggregation metric / named measure (with measures + dimensions) | **`cubes/<name>/metadata.yml`** | New file per cube. Default sink for any `SUM` / `COUNT` / `AVG` / ratio metric raw defines or Lane 3 infers. See `references/cube_proposals.md` for the YAML template, naming policy, duplication guard, and validation flow. Run `wren context validate` + `wren cube query --cube <name> --sql-only` after writing; revert on either failure. **Always escalates to grill in auto-pilot** (Universal Rule 7b). |
|
||||
| Schema structure / relationship / view / model or column description | **MDL YAML** under `models/`, `views/`, `relationships.yml` | Edit the YAML file directly. For catalog #1 / #2 / #3 / #5 / #7 / PII, append a `[tag]` line to `properties.description` (prose first, then one tag per category). See `gap_catalog` for the exact tag format and triggers. |
|
||||
| Aggregation metric / named measure (with measures + dimensions) | **`cubes/<name>/metadata.yml`** | New file per cube. Default sink for any `SUM` / `COUNT` / `AVG` / ratio metric raw defines or Lane 3 infers. See `cube_proposals` for the YAML template, naming policy, duplication guard, and validation flow. Run `wren context validate` + `wren cube query --cube <name> --sql-only` after writing; revert on either failure. **Always escalates to grill in auto-pilot** (Universal Rule 7b). |
|
||||
| Default filter / implicit rule / business convention / naming convention / external mapping / currency / canonical table | **`instructions.md`** | Append under the catalog-specified `##` section heading (#4 → `## Default filters`, #6 → `## Naming conventions`, #8 → `## External identifiers`, #9 → `## Currency`, #10 → `## Canonical tables`). Create the heading if absent; never modify existing text. |
|
||||
| Canonical NL→SQL example the team should share | **`queries.yml`** | Append a new entry under `pairs:` |
|
||||
| Ad-hoc NL→SQL pair (user-local, not for the repo) — **only if `MEMORY_AVAILABLE = true`** | **`wren memory store`** | `wren memory store --nl "..." --sql "..." --tags "source:enrich"` |
|
||||
@@ -334,8 +302,6 @@ If it fails:
|
||||
- MDL YAML uses snake_case keys (e.g. `primary_key`, `is_calculated`, `not_null`). `wren context build` converts to camelCase for `target/mdl.json`.
|
||||
- `instructions.md` is free-form markdown. Group rules by topic with headings.
|
||||
|
||||
---
|
||||
|
||||
## Step 8 — Session finalize
|
||||
|
||||
After Step 6 ends (user says stop in grill mode, or every finding is processed in auto-pilot):
|
||||
@@ -354,8 +320,6 @@ wren memory index
|
||||
|
||||
This re-embeds the new schema items, the updated `instructions.md`, and the new `queries.yml` entries.
|
||||
|
||||
---
|
||||
|
||||
## Step 9 — Summary
|
||||
|
||||
### Both modes — common section
|
||||
@@ -410,8 +374,6 @@ Escalated to grill (raw vs MDL conflicts / high-impact additions):
|
||||
|
||||
The user should be encouraged to skim the audit and either accept it as-is, manually tweak low-confidence rows, or re-run in grill mode if they want to revisit interactively.
|
||||
|
||||
---
|
||||
|
||||
## Things to avoid
|
||||
|
||||
- Do not write a `gaps.yml`, `state.yml`, or any other tracking artifact. The session lives entirely in conversation.
|
||||
@@ -428,15 +390,13 @@ The user should be encouraged to skim the audit and either accept it as-is, manu
|
||||
- Do not append a `[tag]` line if the same category tag already exists for that column — Universal Rule 1. Surface contradictions on the manual-fix list instead.
|
||||
- Do not invent new `instructions.md` section headings. Stick to the five catalog-defined headings (`## Default filters`, `## Naming conventions`, `## External identifiers`, `## Currency`, `## Canonical tables`). Anything that doesn't fit goes on the manual-fix list.
|
||||
- Do not probe the live DB in auto-pilot mode. Step 4.5 is grill-only by default.
|
||||
- Do not propose a cube whose measure expression already exists in another cube on the same `base_object` — write a `queries.yml` example pointing at the existing cube instead. See `references/cube_proposals.md` duplication guard.
|
||||
- Do not propose a cube whose measure expression already exists in another cube on the same `base_object` — write a `queries.yml` example pointing at the existing cube instead. See `cube_proposals` duplication guard.
|
||||
- Do not modify an existing cube YAML even when raw contradicts it — Universal Rule 1. Surface on the manual-fix list.
|
||||
- Do not write a new cube alongside an old MDL `metrics:` entry that already covers the same logic. Surface as "consider migrating to cube" on the manual-fix list.
|
||||
- Do not skip `wren cube query --cube <name> --sql-only` after creating a cube. Structural `wren context validate` doesn't catch unresolvable measure / dimension expressions.
|
||||
- In auto-pilot, do not auto-apply Lane 2 conflicts or new metric / view / relationship inferences — always drop into grill for those.
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
- [`references/gap_catalog.md`](references/gap_catalog.md) — the ten business-semantic gap categories, with triggers, default sinks, and write formats. Read this before Step 4.5 and Step 5.
|
||||
- [`references/cube_proposals.md`](references/cube_proposals.md) — decision tree for when to propose a cube vs view vs calculated column, the cube YAML template, naming policy, duplication guard, and validation flow. Read this before any Lane 3 aggregation-shaped proposal.
|
||||
- `gap_catalog` — the ten business-semantic gap categories, with triggers, default sinks, and write formats. Read this before Step 4.5 and Step 5.
|
||||
- `cube_proposals` — decision tree for when to propose a cube vs view vs calculated column, the cube YAML template, naming policy, duplication guard, and validation flow. Read this before any Lane 3 aggregation-shaped proposal.
|
||||
+5
-45
@@ -1,47 +1,25 @@
|
||||
---
|
||||
name: wren-generate-mdl
|
||||
name: generate-mdl
|
||||
description: "Generate a Wren MDL project by exploring a database with available tools (SQLAlchemy, database drivers, MCP connectors, or raw SQL). Guides agents through schema discovery, type normalization, and MDL YAML generation using the wren CLI. Use when: user wants to create or set up a new MDL, onboard a new data source, or scaffold a project from an existing database."
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: wrenai
|
||||
version: "2.3"
|
||||
---
|
||||
|
||||
# Generate Wren MDL — CLI Agent Workflow
|
||||
|
||||
## Version check
|
||||
|
||||
Silently fetch `https://raw.githubusercontent.com/Canner/WrenAI/main/skills/versions.json`.
|
||||
Compare the `wren-generate-mdl` key with this skill's version (from the frontmatter above).
|
||||
If the remote version is newer, notify the user before proceeding:
|
||||
|
||||
> A newer version of the **wren-generate-mdl** skill is available.
|
||||
> Update with:
|
||||
> ```
|
||||
> npx skills add Canner/WrenAI --skill wren-generate-mdl
|
||||
> ```
|
||||
> The CLI auto-detects your installed agent. To target a specific one, add `--agent <name>` (e.g., `claude-code`, `cursor`, `windsurf`, `cline`).
|
||||
|
||||
Then continue with the workflow below regardless of update status.
|
||||
|
||||
---
|
||||
|
||||
Builds an MDL project by discovering database schema and converting it
|
||||
into Wren's YAML project format. The agent uses whatever database tools
|
||||
are available in its environment for introspection; the wren CLI handles
|
||||
type normalization, validation, and build.
|
||||
|
||||
For memory and query workflows after setup, see the **wren-usage** skill.
|
||||
|
||||
---
|
||||
For memory and query workflows after setup, run `wren skills get usage`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `wren` CLI installed (`pip install "wrenai[<datasource>]"`)
|
||||
- A working database connection (credentials available to the agent)
|
||||
- A wren profile configured (`wren profile add`) or connection info ready
|
||||
|
||||
---
|
||||
- A connection profile (set up via `wren profile add`) or connection info ready
|
||||
|
||||
## Phase 0 — Detect existing project
|
||||
|
||||
@@ -62,8 +40,6 @@ Check whether `wren_project.yml` exists in the current working directory
|
||||
|
||||
If no existing project is detected, proceed directly to Phase 1.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Establish connection and scope
|
||||
|
||||
**Goal:** Confirm the agent can reach the database and agree on scope with the user.
|
||||
@@ -79,8 +55,6 @@ If no existing project is detected, proceed directly to Phase 1.
|
||||
- Whether to include **all tables** or a subset
|
||||
- The **datasource type** for wren (e.g., `postgres` (including Aurora), `mysql` (including Aurora), `bigquery`, `snowflake`) — needed for type normalization dialect
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Discover schema
|
||||
|
||||
**Goal:** Collect table names, column names, column types, and constraints.
|
||||
@@ -132,8 +106,6 @@ Note: this goes through the MDL layer, so it only works if you already
|
||||
have a minimal MDL or if the database supports `information_schema` as
|
||||
regular tables. For bootstrapping from zero, Option A or B is preferred.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Normalize types
|
||||
|
||||
**Goal:** Convert raw database types to wren-core-compatible types.
|
||||
@@ -170,8 +142,6 @@ echo '[{"column":"id","raw_type":"int8"},{"column":"name","raw_type":"character
|
||||
| wren utils parse-types --dialect postgres
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Scaffold and write MDL project
|
||||
|
||||
**Goal:** Create the YAML project structure.
|
||||
@@ -197,7 +167,7 @@ project/
|
||||
> "revenue by month" or "top customers", define cubes alongside models —
|
||||
> they give agents a structured query API instead of forcing them to
|
||||
> hand-write `GROUP BY` / `DATE_TRUNC` SQL. See the
|
||||
> [Cube guide](https://github.com/Canner/WrenAI/blob/main/docs/core/guides/modeling/cube.md).
|
||||
> [Cube guide](https://github.com/Canner/WrenAI/blob/main/docs/core/guides/cubes.md).
|
||||
|
||||
> **IMPORTANT: `catalog` and `schema` in `wren_project.yml`**
|
||||
>
|
||||
@@ -270,8 +240,6 @@ Ask the user to describe:
|
||||
These descriptions are indexed by `wren memory index` and significantly
|
||||
improve LLM query accuracy.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Validate and build
|
||||
|
||||
```bash
|
||||
@@ -294,8 +262,6 @@ If validation fails, fix the reported issues and re-run. Common errors:
|
||||
- Relationship referencing non-existent model
|
||||
- Invalid column type (try re-running through `parse_type`)
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Initialize memory
|
||||
|
||||
```bash
|
||||
@@ -307,9 +273,7 @@ wren memory status
|
||||
```
|
||||
|
||||
After this step, `wren memory fetch` and `wren memory recall` are
|
||||
operational. See the **wren-usage** skill for query workflows.
|
||||
|
||||
---
|
||||
operational. See `wren skills get usage` for query workflows.
|
||||
|
||||
## Phase 7 — Iterate with the user
|
||||
|
||||
@@ -322,8 +286,6 @@ The initial MDL is a starting point. Improve it by:
|
||||
Each change follows: edit YAML → `wren context validate` →
|
||||
`wren context build` → `wren memory index`.
|
||||
|
||||
---
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Task | Command / Method |
|
||||
@@ -342,8 +304,6 @@ Each change follows: edit YAML → `wren context validate` →
|
||||
| Test query | `wren --sql "SELECT * FROM <model> LIMIT 1"` |
|
||||
| Index memory | `wren memory index` |
|
||||
|
||||
---
|
||||
|
||||
## Things to avoid
|
||||
|
||||
- Do not hardcode database-specific type strings in MDL — always normalize via `parse_type`
|
||||
+8
-21
@@ -1,10 +1,9 @@
|
||||
---
|
||||
name: wren-onboarding
|
||||
name: onboarding
|
||||
description: "Onboard a user to Wren Engine end-to-end. Walks through environment checks, project scaffolding, connection configuration via .env, and first query. Use when: user wants to install Wren Engine, set up a new data source connection, or bootstrap a new project from scratch. Triggers: '/wren-onboarding', 'install wren', 'set up wren engine', 'wren onboarding', 'connect new database to wren'."
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: wrenai
|
||||
version: "2.2"
|
||||
---
|
||||
|
||||
# Wren Onboarding — Agent Workflow
|
||||
@@ -16,25 +15,13 @@ Reference docs (the skill points to these — never duplicate their content):
|
||||
- [`docs/core/guides/connect.md`](https://github.com/Canner/WrenAI/blob/main/docs/core/guides/connect.md) — full connection procedure, **per-datasource setup notes, complete troubleshooting playbook**
|
||||
- [`docs/core/get_started/quickstart.md`](https://github.com/Canner/WrenAI/blob/main/docs/core/get_started/quickstart.md) — bundled `jaffle_shop` demo
|
||||
|
||||
## Version check
|
||||
|
||||
Silently fetch `https://raw.githubusercontent.com/Canner/WrenAI/main/skills/versions.json`. Compare the `wren-onboarding` key with this skill's version (from the frontmatter above). If the remote version is newer, notify the user:
|
||||
|
||||
> A newer version of the **wren-onboarding** skill is available.
|
||||
> Update with:
|
||||
> ```
|
||||
> npx skills add Canner/WrenAI --skill wren-onboarding
|
||||
> ```
|
||||
|
||||
Continue regardless of update status.
|
||||
|
||||
## Mode of operation — READ THIS FIRST
|
||||
|
||||
**One step per round-trip.** Each numbered step below is its own turn: explain briefly, ask **only** what the step needs, run the command(s), confirm, move on.
|
||||
|
||||
- ❌ **Never collect information for future steps upfront.** Do not ask for project name + database type + credentials in one message.
|
||||
- ❌ **Never ask for credentials in chat — not host, port, user, password, tokens, anything.** Credentials always go through `.env`. The user fills the file in their editor; the agent never sees the values.
|
||||
- ❌ **Never query the database before MDL is built** via the `wren-generate-mdl` skill.
|
||||
- ❌ **Never query the database before MDL is built** via `wren skills get generate-mdl`.
|
||||
- ❌ **Never invent connection field names.** Always run `wren docs connection-info <ds>` to see the real fields — it's introspected from the live Pydantic schema, so it's always correct.
|
||||
- ✅ Wait for each command to finish, report its output in plain language, then move on.
|
||||
- ✅ For any error, consult `connect.md#troubleshooting` and surface the relevant section to the user — don't carry a copy of the playbook here.
|
||||
@@ -142,7 +129,7 @@ This step also future-proofs the project for multi-project setups: once the bind
|
||||
|
||||
> ⚠️ The agent **must** build MDL before any data query. Queries against tables not in MDL will fail.
|
||||
|
||||
Invoke the **`wren-generate-mdl`** skill. It walks the agent through table introspection, type normalization, and YAML generation. When it finishes, return here and run:
|
||||
Run `wren skills get generate-mdl` and follow it. It walks the agent through table introspection, type normalization, and YAML generation. When it finishes, return here and run:
|
||||
|
||||
```bash
|
||||
wren context validate
|
||||
@@ -155,16 +142,16 @@ Report the model count and any validate warnings.
|
||||
|
||||
## Step 5 — Ready to explore (hand off)
|
||||
|
||||
Suggest 2–3 NL questions based on the discovered tables (e.g. for an orders schema: "How many orders last month?", "Top 5 customers by total"). Then end this skill: for day-to-day querying the agent should switch to the **`wren-usage`** skill.
|
||||
Suggest 2–3 NL questions based on the discovered tables (e.g. for an orders schema: "How many orders last month?", "Top 5 customers by total"). Then end this skill: for day-to-day querying the agent should run `wren skills get usage`.
|
||||
|
||||
## Cross-skill routing
|
||||
|
||||
| Trigger | Skill |
|
||||
|---------|-------|
|
||||
| User mentions a SaaS source (HubSpot, Stripe, Salesforce, GitHub, Slack, …) | `wren-dlt-connector` |
|
||||
| User has a connected DB but no MDL yet | `wren-generate-mdl` |
|
||||
| User has MDL ready, wants to query | `wren-usage` |
|
||||
| Anything else from-scratch | `wren-onboarding` (this skill) |
|
||||
| User mentions a SaaS source (HubSpot, Stripe, Salesforce, GitHub, Slack, …) | `wren skills get dlt-connector` |
|
||||
| User has a connected DB but no MDL yet | `wren skills get generate-mdl` |
|
||||
| User has MDL ready, wants to query | `wren skills get usage` |
|
||||
| Anything else from-scratch | `wren skills get onboarding` (this skill) |
|
||||
|
||||
## On error
|
||||
|
||||
@@ -1,30 +1,14 @@
|
||||
---
|
||||
name: wren-usage
|
||||
name: usage
|
||||
description: "Wren Engine CLI workflow guide for AI agents. Answer data questions end-to-end using the wren CLI: gather schema context, recall past queries, write SQL through the MDL semantic layer, execute, and learn from confirmed results. Use when: user asks a data question, requests a report or analysis, asks about metrics, revenue, customers, orders, trends, or any business data; user says 'how many', 'show me', 'what is the', 'top N', 'compare', 'trend', 'growth', 'breakdown'; user wants to explore, analyze, filter, aggregate, or summarize data from a database; agent needs to query data, connect a data source, handle errors, or manage MDL changes via the wren CLI."
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: wrenai
|
||||
version: "2.4"
|
||||
---
|
||||
|
||||
# Wren Engine CLI — Agent Workflow Guide
|
||||
|
||||
## Version check
|
||||
|
||||
Silently fetch `https://raw.githubusercontent.com/Canner/WrenAI/main/skills/versions.json`.
|
||||
Compare the `wren-usage` key with this skill's version (from the frontmatter above).
|
||||
If the remote version is newer, notify the user before proceeding:
|
||||
|
||||
> A newer version of the **wren-usage** skill is available.
|
||||
> Update with:
|
||||
> ```
|
||||
> npx skills add Canner/WrenAI --skill wren-usage
|
||||
> ```
|
||||
> The CLI auto-detects your installed agent. To target a specific one, add `--agent <name>` (e.g., `claude-code`, `cursor`, `windsurf`, `cline`).
|
||||
|
||||
Then continue with the workflow below regardless of update status.
|
||||
|
||||
---
|
||||
> This guide is served by the `wren` CLI (`wren skills get usage`), so it always matches your installed wren-engine version. Pull the deeper reference docs with `wren skills get usage --full`.
|
||||
|
||||
## Preflight — Verify environment and installation
|
||||
|
||||
@@ -88,8 +72,8 @@ Two things drive everything:
|
||||
|
||||
The CLI reads the active profile for connection info and datasource. Use `wren profile list` to see which profile is active, `wren profile switch <name>` to change it. `dry-plan` also accepts `--datasource` / `-d` for transpile-only use without a profile.
|
||||
|
||||
For memory-specific decisions, see [references/memory.md](references/memory.md).
|
||||
For SQL syntax, CTE-based modeling, and error diagnosis, see [references/wren-sql.md](references/wren-sql.md).
|
||||
For memory-specific decisions, see the `memory` reference (run `wren skills get usage --full`).
|
||||
For SQL syntax, CTE-based modeling, and error diagnosis, see the `wren-sql` reference (run `wren skills get usage --full`).
|
||||
For project structure, MDL field definitions, and CLI workflow details, see the [documentation](https://github.com/Canner/WrenAI/tree/main/docs/core).
|
||||
|
||||
---
|
||||
@@ -264,7 +248,7 @@ The DB error + dry-plan output together pinpoint the issue:
|
||||
the query to the smallest failing fragment. Execute subqueries independently
|
||||
to isolate which part fails.
|
||||
|
||||
For the CTE rewrite pipeline and additional error patterns, see [references/wren-sql.md](references/wren-sql.md).
|
||||
For the CTE rewrite pipeline and additional error patterns, see the `wren-sql` reference (run `wren skills get usage --full`).
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"""Serve bundled agent skill content from package data.
|
||||
|
||||
Skill content ships inside the wheel under ``wren/skills_content/<name>/``.
|
||||
``wren skills get <name>`` returns the skill's ``SKILL.md`` main guide. Deeper
|
||||
``references/`` and bundled ``scripts/`` are surfaced by ``wren skills list``
|
||||
and (in a follow-up slice) delivered via ``--full`` / ``--script``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from importlib import resources
|
||||
|
||||
import yaml
|
||||
|
||||
_CONTENT_DIR = "skills_content"
|
||||
|
||||
|
||||
class SkillNotFoundError(Exception):
|
||||
"""Raised when a requested skill name has no bundled content."""
|
||||
|
||||
|
||||
class ScriptNotFoundError(Exception):
|
||||
"""Raised when a requested script is not bundled with the skill."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class SkillInfo:
|
||||
name: str
|
||||
summary: str
|
||||
references: list[str] = field(default_factory=list)
|
||||
scripts: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def _content_root():
|
||||
"""Traversable for ``wren/skills_content/`` (anchored on the ``wren`` package)."""
|
||||
return resources.files("wren") / _CONTENT_DIR
|
||||
|
||||
|
||||
def _skill_dir(name: str):
|
||||
root = _content_root()
|
||||
skill = root / name
|
||||
if not (skill.is_dir() and (skill / "SKILL.md").is_file()):
|
||||
raise SkillNotFoundError(name)
|
||||
return skill
|
||||
|
||||
|
||||
def get_skill(name: str, full: bool = False) -> str:
|
||||
"""Return the ``SKILL.md`` main guide for ``name``.
|
||||
|
||||
With ``full=True``, append every ``references/*.md`` (sorted by filename)
|
||||
after the main guide, each under a separator heading. Skills with no
|
||||
references return the main guide unchanged.
|
||||
"""
|
||||
skill = _skill_dir(name)
|
||||
content = (skill / "SKILL.md").read_text(encoding="utf-8")
|
||||
if not full:
|
||||
return content
|
||||
refs_dir = skill / "references"
|
||||
if not refs_dir.is_dir():
|
||||
return content
|
||||
parts = [content.rstrip()]
|
||||
for ref in sorted(
|
||||
(p for p in refs_dir.iterdir() if p.name.endswith(".md")),
|
||||
key=lambda p: p.name,
|
||||
):
|
||||
body = ref.read_text(encoding="utf-8").strip()
|
||||
parts.append(f"# Reference: {ref.name[:-3]}\n\n{body}")
|
||||
return "\n\n---\n\n".join(parts) + "\n"
|
||||
|
||||
|
||||
def get_script(name: str, script: str) -> str:
|
||||
"""Return the source of a script bundled under ``<skill>/scripts/``."""
|
||||
scripts_dir = _skill_dir(name) / "scripts"
|
||||
if scripts_dir.is_dir():
|
||||
for path in scripts_dir.iterdir():
|
||||
if path.is_file() and path.name.rsplit(".", 1)[0] == script:
|
||||
return path.read_text(encoding="utf-8")
|
||||
raise ScriptNotFoundError(f"{name}/{script}")
|
||||
|
||||
|
||||
def list_skills() -> list[SkillInfo]:
|
||||
"""List every bundled skill, sorted by name."""
|
||||
root = _content_root()
|
||||
out: list[SkillInfo] = []
|
||||
for entry in sorted(root.iterdir(), key=lambda p: p.name):
|
||||
if not entry.is_dir() or not (entry / "SKILL.md").is_file():
|
||||
continue
|
||||
out.append(
|
||||
SkillInfo(
|
||||
name=entry.name,
|
||||
summary=_summary((entry / "SKILL.md").read_text(encoding="utf-8")),
|
||||
references=_md_stems(entry / "references"),
|
||||
scripts=_script_stems(entry / "scripts"),
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
_SUMMARY_MAX = 100
|
||||
|
||||
|
||||
def _summary(skill_md_text: str) -> str:
|
||||
"""A short one-line summary from the frontmatter ``description``."""
|
||||
desc = _frontmatter_field(skill_md_text, "description")
|
||||
if not desc:
|
||||
return ""
|
||||
summary = desc.split(". ", 1)[0].rstrip(".")
|
||||
if len(summary) > _SUMMARY_MAX:
|
||||
summary = summary[: _SUMMARY_MAX - 1].rstrip() + "…"
|
||||
return summary
|
||||
|
||||
|
||||
def _frontmatter_field(text: str, key: str) -> str | None:
|
||||
if not text.startswith("---"):
|
||||
return None
|
||||
end = text.find("\n---", 3)
|
||||
if end == -1:
|
||||
return None
|
||||
try:
|
||||
data = yaml.safe_load(text[3:end]) or {}
|
||||
except yaml.YAMLError:
|
||||
return None
|
||||
value = data.get(key)
|
||||
return value if isinstance(value, str) else None
|
||||
|
||||
|
||||
def _md_stems(directory) -> list[str]:
|
||||
if not directory.is_dir():
|
||||
return []
|
||||
return sorted(p.name[:-3] for p in directory.iterdir() if p.name.endswith(".md"))
|
||||
|
||||
|
||||
def _script_stems(directory) -> list[str]:
|
||||
if not directory.is_dir():
|
||||
return []
|
||||
return sorted(
|
||||
p.name.rsplit(".", 1)[0]
|
||||
for p in directory.iterdir()
|
||||
if p.is_file() and p.name.rsplit(".", 1)[-1] in ("py", "sh")
|
||||
)
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Tests for `wren ask` prompt shaping."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from wren import ask as ask_mod
|
||||
from wren.cli import app
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_no_mode_flag_rejected():
|
||||
result = runner.invoke(app, ["ask", "show me revenue"])
|
||||
assert result.exit_code != 0
|
||||
out = result.output + (result.stderr if result.stderr_bytes else "")
|
||||
assert "--guided" in out and "--direct" in out
|
||||
|
||||
|
||||
def test_both_mode_flags_rejected():
|
||||
result = runner.invoke(app, ["ask", "show me revenue", "--guided", "--direct"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
|
||||
def test_guided_includes_task_flow_and_substitutes_prompt():
|
||||
result = runner.invoke(app, ["ask", "top 5 customers by revenue", "--guided"])
|
||||
assert result.exit_code == 0
|
||||
assert "TASK TYPE A" in result.output
|
||||
assert "TASK TYPE B" in result.output
|
||||
assert "wren context show" in result.output
|
||||
assert "top 5 customers by revenue" in result.output
|
||||
assert "<USER_PROMPT>" not in result.output # placeholder substituted
|
||||
|
||||
|
||||
def test_direct_minimal_and_substitutes_prompt():
|
||||
result = runner.invoke(app, ["ask", "monthly orders trend", "--direct"])
|
||||
assert result.exit_code == 0
|
||||
assert "wren skills list" in result.output
|
||||
assert "wren --help" in result.output
|
||||
assert "monthly orders trend" in result.output
|
||||
assert "<USER_PROMPT>" not in result.output
|
||||
# direct mode should NOT include the guided TASK TYPE structure
|
||||
assert "TASK TYPE A" not in result.output
|
||||
|
||||
|
||||
def test_render_api_known_modes():
|
||||
for mode in ask_mod.MODES:
|
||||
out = ask_mod.render(mode, "hello world")
|
||||
assert "hello world" in out
|
||||
assert "<USER_PROMPT>" not in out
|
||||
|
||||
|
||||
def test_render_unknown_mode_raises():
|
||||
with pytest.raises(ask_mod.UnknownAskModeError):
|
||||
ask_mod.render("auto", "anything")
|
||||
|
||||
|
||||
def test_user_prompt_with_template_placeholder_substring_is_safe():
|
||||
# Prompt containing the literal placeholder shouldn't break rendering;
|
||||
# we only do one replacement of the bundled-template placeholder.
|
||||
prompt = "Show literal <USER_PROMPT> usage examples"
|
||||
out = ask_mod.render("direct", prompt)
|
||||
# the bundled placeholder is gone and the prompt is present (verbatim)
|
||||
assert prompt in out
|
||||
@@ -0,0 +1,288 @@
|
||||
"""Guard: every `wren <cmd>` mentioned in bundled skill/docs/template content
|
||||
must resolve to a real CLI command (and `--flags` mentioned must be real flags
|
||||
on that command).
|
||||
|
||||
This catches the "forward reference" failure mode we hit during incremental
|
||||
rollout — e.g., a lifted skill mentioning `wren docs get` before that command
|
||||
exists. By the time the branch is ready to ship, every command and flag the
|
||||
served content tells an agent to run must actually exist in the CLI.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
import pytest
|
||||
import typer
|
||||
|
||||
from wren.cli import app
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
# ── Build the real command tree ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def _flags(cmd) -> set[str]:
|
||||
flags: set[str] = set()
|
||||
for param in getattr(cmd, "params", []):
|
||||
if isinstance(param, click.Option):
|
||||
for opt in param.opts + param.secondary_opts:
|
||||
if opt.startswith("--"):
|
||||
flags.add(opt)
|
||||
return flags
|
||||
|
||||
|
||||
def _walk(cmd, prefix: str = "") -> dict[str, set[str]]:
|
||||
"""Map command-path-string -> set of long flags.
|
||||
|
||||
Uses ``cmd.commands`` directly rather than ``cmd.list_commands(ctx=None)``
|
||||
because the latter passes a ``None`` context — Click 8.4+ tightened that
|
||||
path and started returning ``[]``, which would silently collapse this
|
||||
map to the root command and neuter the guard.
|
||||
"""
|
||||
out: dict[str, set[str]] = {prefix.strip(): _flags(cmd)}
|
||||
if isinstance(cmd, click.Group):
|
||||
for name, sub in cmd.commands.items():
|
||||
out.update(_walk(sub, f"{prefix} {name}".strip()))
|
||||
return out
|
||||
|
||||
|
||||
COMMANDS: dict[str, set[str]] = _walk(typer.main.get_command(app))
|
||||
|
||||
# ``wren memory`` is conditionally registered (needs `wren-engine[memory]`
|
||||
# extras). It's a real public surface; bundled content correctly references
|
||||
# it. Allow-list its subcommands so this guard doesn't flag false positives
|
||||
# when the test env lacks memory extras.
|
||||
_MEMORY_SUBCOMMANDS = (
|
||||
"index",
|
||||
"describe",
|
||||
"fetch",
|
||||
"store",
|
||||
"recall",
|
||||
"status",
|
||||
"reset",
|
||||
"list",
|
||||
"forget",
|
||||
"dump",
|
||||
"load",
|
||||
)
|
||||
COMMANDS.setdefault("memory", set())
|
||||
for _s in _MEMORY_SUBCOMMANDS:
|
||||
COMMANDS.setdefault(f"memory {_s}", set())
|
||||
|
||||
# Typer/Click adds these to every command implicitly.
|
||||
_UNIVERSAL_FLAGS = {"--help"}
|
||||
|
||||
# The first token after `wren` must be one of these (commands or top-level
|
||||
# flags) for the snippet to count as a CLI invocation worth validating. Otherwise
|
||||
# it's prose ("the wren engine connects to ...", "in the wren project layout").
|
||||
_TOP_LEVEL_COMMANDS: set[str] = {p.split()[0] for p in COMMANDS if p}
|
||||
|
||||
# Flags on `wren memory ...` cannot be introspected here without the memory
|
||||
# extras installed (lancedb + sentence-transformers). Skip flag validation for
|
||||
# memory commands; the command path itself is still validated via the
|
||||
# allow-list above.
|
||||
_SKIP_FLAG_VALIDATION_FOR_GROUPS = {"memory"}
|
||||
|
||||
|
||||
# ── Scan served content ─────────────────────────────────────────────────────
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[4]
|
||||
_SKILLS_CONTENT = _REPO / "core" / "wren" / "src" / "wren" / "skills_content"
|
||||
_DOCS_CONTENT = _REPO / "core" / "wren" / "src" / "wren" / "docs_content"
|
||||
_ASK_TEMPLATES = _REPO / "core" / "wren" / "src" / "wren" / "ask_templates"
|
||||
# The discovery stub ships to users' local skill dirs via `npx skills add`,
|
||||
# so any `wren <cmd>` it references must resolve to a real CLI command too.
|
||||
_DISCOVERY_STUB = _REPO / "skills" / "wren" / "SKILL.md"
|
||||
|
||||
# Match `wren ` followed by content up to a newline, backtick, single/double
|
||||
# quote (avoid consuming SQL strings), or closing paren.
|
||||
_INVOCATION = re.compile(r"\bwren\s+(?P<rest>[^\n`'\"\)]+)")
|
||||
|
||||
# A valid command-name token: lowercase alphanumerics + hyphens.
|
||||
_TOKEN = re.compile(r"^[a-z][a-z0-9-]*$")
|
||||
|
||||
|
||||
def _iter_content_files() -> list[Path]:
|
||||
"""Yield every served-content file the guard must validate.
|
||||
|
||||
Covers the package-data roots (`skills_content`, `ask_templates`) plus
|
||||
the standalone discovery stub at `skills/wren/` that's distributed to
|
||||
users via `npx skills add`. (`docs_content` is kept in the scan list as
|
||||
a no-op — the dir no longer ships, so it's skipped when absent.)
|
||||
"""
|
||||
files: list[Path] = []
|
||||
for root in (_SKILLS_CONTENT, _DOCS_CONTENT, _ASK_TEMPLATES):
|
||||
if not root.is_dir():
|
||||
continue
|
||||
for path in sorted(root.rglob("*")):
|
||||
if path.is_file() and path.suffix in (".md", ".tmpl"):
|
||||
files.append(path)
|
||||
if _DISCOVERY_STUB.is_file():
|
||||
files.append(_DISCOVERY_STUB)
|
||||
return files
|
||||
|
||||
|
||||
def _enumerate_invocations() -> list[tuple[Path, str, list[str]]]:
|
||||
"""Return (file, raw_snippet, tokens) for every wren invocation in served content."""
|
||||
out: list[tuple[Path, str, list[str]]] = []
|
||||
for path in _iter_content_files():
|
||||
text = path.read_text(encoding="utf-8")
|
||||
# Join shell backslash-newline continuations so flags on follow-up
|
||||
# lines (e.g. `wren cube query \\\n --measures revenue`) are part
|
||||
# of the same captured invocation rather than silently dropped.
|
||||
text = re.sub(r"\\\s*\n[ \t]*", " ", text)
|
||||
for m in _INVOCATION.finditer(text):
|
||||
snippet = m.group("rest").strip()
|
||||
tokens = snippet.split()
|
||||
if not tokens:
|
||||
continue
|
||||
out.append((path, snippet, tokens))
|
||||
return out
|
||||
|
||||
|
||||
def _resolve(tokens: list[str]) -> tuple[str, list[str]]:
|
||||
"""Walk the command tree as long as the next token is a sub-command.
|
||||
Returns (command_path, remaining_tokens).
|
||||
"""
|
||||
path = ""
|
||||
i = 0
|
||||
while i < len(tokens):
|
||||
tok = tokens[i]
|
||||
# stop walking on placeholders, flags, quoted args
|
||||
if not _TOKEN.match(tok):
|
||||
break
|
||||
candidate = (path + " " + tok).strip() if path else tok
|
||||
# extend path if candidate is a registered command OR a prefix of one
|
||||
if candidate in COMMANDS or any(
|
||||
p == candidate or p.startswith(candidate + " ") for p in COMMANDS
|
||||
):
|
||||
path = candidate
|
||||
i += 1
|
||||
else:
|
||||
break
|
||||
return path, tokens[i:]
|
||||
|
||||
|
||||
def _findings() -> list[str]:
|
||||
problems: list[str] = []
|
||||
for path, snippet, tokens in _enumerate_invocations():
|
||||
first = tokens[0]
|
||||
# Only treat as a CLI invocation if first token is a known top-level
|
||||
# command/group OR a top-level flag (e.g. `wren --sql ...`). Otherwise
|
||||
# it's prose ("the wren engine", "in the wren project layout") — skip.
|
||||
if not first.startswith("-") and first not in _TOP_LEVEL_COMMANDS:
|
||||
continue
|
||||
|
||||
cmd_path, leftover = _resolve(tokens)
|
||||
try:
|
||||
rel = path.relative_to(_REPO)
|
||||
except ValueError:
|
||||
rel = path
|
||||
|
||||
# Unknown-subcommand check. If `cmd_path` resolved to a group node
|
||||
# (has at least one child registered in COMMANDS) and the first
|
||||
# leftover token is a plain word, that word is an unknown
|
||||
# subcommand the user typo'd — `wren docs typo`, `wren memory typo`.
|
||||
# On a leaf command (`skills get`, `docs get`) leftover words are
|
||||
# positional args, not typos, so the children gate skips them.
|
||||
# Runs before _SKIP_FLAG_VALIDATION_FOR_GROUPS so typos under
|
||||
# `memory` are caught even though we can't introspect memory flags.
|
||||
if (
|
||||
leftover
|
||||
and _TOKEN.match(leftover[0])
|
||||
and any(p.startswith(f"{cmd_path} ") for p in COMMANDS if cmd_path)
|
||||
):
|
||||
problems.append(
|
||||
f"{rel}: unknown subcommand '{leftover[0]}' for 'wren {cmd_path}'"
|
||||
+ f" (in `wren {snippet}`)"
|
||||
)
|
||||
continue
|
||||
|
||||
# Skip flag validation for groups we can't introspect (memory needs
|
||||
# extras installed); the command path is still validated above by the
|
||||
# allow-list — only flags are skipped.
|
||||
cmd_group = cmd_path.split()[0] if cmd_path else ""
|
||||
if cmd_group in _SKIP_FLAG_VALIDATION_FOR_GROUPS:
|
||||
continue
|
||||
|
||||
for tok in leftover:
|
||||
if not tok.startswith("--"):
|
||||
continue
|
||||
flag = tok.rstrip(",.;:")
|
||||
if flag in _UNIVERSAL_FLAGS:
|
||||
continue
|
||||
allowed = COMMANDS.get(cmd_path, set())
|
||||
if flag not in allowed:
|
||||
problems.append(
|
||||
f"{rel}: unknown flag '{flag}' for 'wren {cmd_path}'".rstrip()
|
||||
+ f" (in `wren {snippet}`)"
|
||||
)
|
||||
return problems
|
||||
|
||||
|
||||
# ── Test surface ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_command_tree_loaded():
|
||||
"""Sanity: introspection finds the commands we expect."""
|
||||
assert "skills get" in COMMANDS
|
||||
assert "docs connection-info" in COMMANDS
|
||||
assert "ask" in COMMANDS
|
||||
assert "--full" in COMMANDS["skills get"]
|
||||
assert "--script" in COMMANDS["skills get"]
|
||||
assert "--guided" in COMMANDS["ask"]
|
||||
assert "--direct" in COMMANDS["ask"]
|
||||
|
||||
|
||||
def test_served_content_invocations_resolve():
|
||||
problems = _findings()
|
||||
if problems:
|
||||
msg = (
|
||||
"Served content references commands/flags that don't exist:\n "
|
||||
+ "\n ".join(problems)
|
||||
)
|
||||
pytest.fail(msg)
|
||||
|
||||
|
||||
def test_at_least_one_invocation_was_validated():
|
||||
"""Sanity: ensure the scanner actually finds invocations to validate
|
||||
(so a regression in the regex doesn't silently pass)."""
|
||||
invocations = _enumerate_invocations()
|
||||
# Current served content yields ~400 invocations. A regression in the
|
||||
# scanner that silently dropped most matches would weaken the guard
|
||||
# without obviously failing — keep the floor tight enough to notice.
|
||||
assert len(invocations) >= 200, (
|
||||
f"only found {len(invocations)} wren invocations; regex may be broken"
|
||||
)
|
||||
|
||||
|
||||
def test_unknown_subcommand_is_flagged(tmp_path, monkeypatch):
|
||||
"""An unknown subcommand under a known group (`wren docs typo`) must
|
||||
be reported. Leaf commands taking positional args (`wren skills get
|
||||
usage`) must NOT be flagged. Typos under `memory` must also be caught
|
||||
even though flag validation is skipped for memory.
|
||||
"""
|
||||
fake_skill = tmp_path / "skill.md"
|
||||
fake_skill.write_text(
|
||||
"Run `wren docs typo` to break stuff.\n"
|
||||
"Also `wren skills get usage` — this is legit (positional, not typo).\n"
|
||||
"And `wren memory typo` — should also be flagged.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
module = sys.modules[__name__]
|
||||
monkeypatch.setattr(module, "_SKILLS_CONTENT", tmp_path)
|
||||
monkeypatch.setattr(module, "_DOCS_CONTENT", tmp_path / "_empty_docs")
|
||||
monkeypatch.setattr(module, "_ASK_TEMPLATES", tmp_path / "_empty_ask")
|
||||
monkeypatch.setattr(module, "_DISCOVERY_STUB", tmp_path / "_no_stub.md")
|
||||
|
||||
problems = _findings()
|
||||
joined = "\n".join(problems)
|
||||
assert "unknown subcommand 'typo' for 'wren docs'" in joined, problems
|
||||
assert "unknown subcommand 'typo' for 'wren memory'" in joined, problems
|
||||
# leaf + positional should NOT appear
|
||||
assert "'usage'" not in joined, problems
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Guard tests for the skills/ distribution stubs.
|
||||
|
||||
The new model:
|
||||
- skills/wren/SKILL.md is the single discovery stub that lists every CLI surface.
|
||||
- The five previously-shipped fat skills (and their one-release redirect
|
||||
stubs) are gone — agents fetch workflow guides via `wren skills get`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[4]
|
||||
_SKILLS = _REPO / "skills"
|
||||
|
||||
DEPRECATED_DIRS = [
|
||||
"wren-onboarding",
|
||||
"wren-usage",
|
||||
"wren-generate-mdl",
|
||||
"wren-dlt-connector",
|
||||
"wren-enrich-context",
|
||||
]
|
||||
|
||||
|
||||
def test_discovery_stub_exists():
|
||||
stub = _SKILLS / "wren" / "SKILL.md"
|
||||
assert stub.is_file()
|
||||
text = stub.read_text()
|
||||
assert text.startswith("---")
|
||||
assert "allowed-tools:" in text
|
||||
# discovery stub points at the CLI surfaces
|
||||
assert "wren skills list" in text
|
||||
assert "wren docs connection-info" in text
|
||||
assert "wren ask" in text
|
||||
|
||||
|
||||
def test_deprecated_dirs_removed():
|
||||
"""The five deprecated redirect-stub dirs must NOT come back: regenerating
|
||||
them would reintroduce the version-drift problem the new model exists to
|
||||
solve."""
|
||||
for name in DEPRECATED_DIRS:
|
||||
assert not (_SKILLS / name).exists(), (
|
||||
f"skills/{name}/ was removed when the redirect window closed; "
|
||||
"do not reintroduce. Content lives in core/wren/src/wren/"
|
||||
f"skills_content/{name.removeprefix('wren-')}/."
|
||||
)
|
||||
|
||||
|
||||
def test_versions_json_removed():
|
||||
assert not (_SKILLS / "versions.json").exists(), (
|
||||
"versions.json should be deleted (version drift impossible in the new model)"
|
||||
)
|
||||
assert not (_SKILLS / "check-versions.sh").exists(), (
|
||||
"check-versions.sh should be deleted (no per-skill versions anymore)"
|
||||
)
|
||||
|
||||
|
||||
def test_install_sh_installs_discovery_stub():
|
||||
install = (_SKILLS / "install.sh").read_text()
|
||||
assert 'SKILL="wren"' in install, (
|
||||
"install.sh must install the new `wren` discovery stub"
|
||||
)
|
||||
|
||||
|
||||
def test_index_json_lists_only_discovery_stub():
|
||||
data = json.loads((_SKILLS / "index.json").read_text())
|
||||
names = [s["name"] for s in data["skills"]]
|
||||
assert names == ["wren"], (
|
||||
f"index.json must list only the discovery stub, got: {names}"
|
||||
)
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Tests for `wren skills` content delivery (tracer bullet: infra + usage)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from wren import skills_delivery
|
||||
from wren.cli import app
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_skills_list_includes_usage_with_references():
|
||||
result = runner.invoke(app, ["skills", "list"])
|
||||
assert result.exit_code == 0
|
||||
assert "usage" in result.output
|
||||
# usage ships two references; list should surface them
|
||||
assert "memory" in result.output
|
||||
assert "wren-sql" in result.output
|
||||
|
||||
|
||||
def test_skills_get_usage_returns_guide():
|
||||
result = runner.invoke(app, ["skills", "get", "usage"])
|
||||
assert result.exit_code == 0
|
||||
assert result.output.strip().startswith("---") # markdown frontmatter
|
||||
assert "Wren Engine CLI" in result.output
|
||||
|
||||
|
||||
def test_skills_get_usage_is_trimmed():
|
||||
"""Lifted content drops the version-drift hack and the version field."""
|
||||
content = skills_delivery.get_skill("usage")
|
||||
assert "versions.json" not in content
|
||||
assert "## Version check" not in content
|
||||
assert 'version: "2.4"' not in content
|
||||
|
||||
|
||||
def test_skills_get_unknown_errors_with_hint():
|
||||
result = runner.invoke(app, ["skills", "get", "does-not-exist"])
|
||||
assert result.exit_code != 0
|
||||
assert "wren skills list" in result.output
|
||||
|
||||
|
||||
def test_skills_list_api_reports_usage():
|
||||
skills = {s.name: s for s in skills_delivery.list_skills()}
|
||||
assert "usage" in skills
|
||||
assert set(skills["usage"].references) == {"memory", "wren-sql"}
|
||||
assert skills["usage"].summary
|
||||
|
||||
|
||||
def test_skills_get_unknown_raises():
|
||||
with pytest.raises(skills_delivery.SkillNotFoundError):
|
||||
skills_delivery.get_skill("nope")
|
||||
|
||||
|
||||
# ── Ticket 1b: all five skills + --full + --script ──────────────────────────
|
||||
|
||||
ALL_SKILLS = {"onboarding", "usage", "generate-mdl", "dlt-connector", "enrich-context"}
|
||||
|
||||
|
||||
def test_all_five_skills_bundled():
|
||||
names = {s.name for s in skills_delivery.list_skills()}
|
||||
assert names == ALL_SKILLS
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", sorted(ALL_SKILLS))
|
||||
def test_get_each_skill_nonempty_and_trimmed(name):
|
||||
content = skills_delivery.get_skill(name)
|
||||
assert content.strip().startswith("---")
|
||||
# every lifted skill drops the version-drift hack + version field
|
||||
assert "versions.json" not in content
|
||||
assert "## Version check" not in content
|
||||
assert "version:" not in content.split("---", 2)[1] # not in frontmatter
|
||||
|
||||
|
||||
def test_get_each_skill_via_cli(name="usage"):
|
||||
for n in sorted(ALL_SKILLS):
|
||||
result = runner.invoke(app, ["skills", "get", n])
|
||||
assert result.exit_code == 0, n
|
||||
assert result.output.strip()
|
||||
|
||||
|
||||
def test_full_inlines_references_for_enrich_context():
|
||||
plain = skills_delivery.get_skill("enrich-context")
|
||||
full = skills_delivery.get_skill("enrich-context", full=True)
|
||||
assert len(full) > len(plain)
|
||||
assert "# Reference: cube_proposals" in full
|
||||
assert "# Reference: gap_catalog" in full
|
||||
|
||||
|
||||
def test_full_is_graceful_when_no_references():
|
||||
# onboarding has no references/ dir
|
||||
assert skills_delivery.get_skill(
|
||||
"onboarding", full=True
|
||||
) == skills_delivery.get_skill("onboarding")
|
||||
|
||||
|
||||
def test_full_does_not_inline_scripts():
|
||||
full = skills_delivery.get_skill("dlt-connector", full=True)
|
||||
assert "# Reference: dlt_sources" in full
|
||||
assert "#!/usr/bin/env python3" not in full # the script is not inlined by --full
|
||||
|
||||
|
||||
def test_get_script_returns_source():
|
||||
src = skills_delivery.get_script("dlt-connector", "introspect_dlt")
|
||||
assert src.startswith("#!/usr/bin/env python3")
|
||||
assert "introspect" in src
|
||||
|
||||
|
||||
def test_get_script_via_cli():
|
||||
result = runner.invoke(
|
||||
app, ["skills", "get", "dlt-connector", "--script", "introspect_dlt"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "#!/usr/bin/env python3" in result.output
|
||||
|
||||
|
||||
def test_get_unknown_script_errors():
|
||||
result = runner.invoke(app, ["skills", "get", "dlt-connector", "--script", "nope"])
|
||||
assert result.exit_code != 0
|
||||
with pytest.raises(skills_delivery.ScriptNotFoundError):
|
||||
skills_delivery.get_script("dlt-connector", "nope")
|
||||
|
||||
|
||||
def test_list_reports_references_and_scripts():
|
||||
by_name = {s.name: s for s in skills_delivery.list_skills()}
|
||||
assert set(by_name["enrich-context"].references) == {
|
||||
"cube_proposals",
|
||||
"gap_catalog",
|
||||
}
|
||||
assert by_name["dlt-connector"].scripts == ["introspect_dlt"]
|
||||
assert by_name["onboarding"].references == []
|
||||
@@ -96,7 +96,9 @@ Requires `target/manifest.json` and `target/catalog.json`; run `dbt build` and `
|
||||
|
||||
---
|
||||
|
||||
## `wren docs` — Connection Info Reference
|
||||
## `wren docs` — Connection Info
|
||||
|
||||
### `wren docs connection-info <datasource>`
|
||||
|
||||
Print the required and optional connection fields for a data source.
|
||||
|
||||
@@ -295,3 +297,74 @@ cat query.json | wren cube query --from -
|
||||
|
||||
See the [Cube guide](../guides/cubes.md) for YAML structure and
|
||||
validation rules.
|
||||
|
||||
---
|
||||
|
||||
## `wren skills` — Agent Workflow Guides
|
||||
|
||||
The CLI ships its own agent skill content. Use this on any AI client (the
|
||||
content is the same — content travels with the wheel, not the agent cache).
|
||||
|
||||
### `wren skills list`
|
||||
|
||||
List the available workflow guides.
|
||||
|
||||
```bash
|
||||
wren skills list
|
||||
```
|
||||
|
||||
### `wren skills get <name>`
|
||||
|
||||
Print a skill's main guide to stdout. Five names ship today:
|
||||
`onboarding`, `usage`, `generate-mdl`, `dlt-connector`, `enrich-context`.
|
||||
|
||||
```bash
|
||||
wren skills get onboarding # set up Wren end-to-end
|
||||
wren skills get usage # day-to-day querying
|
||||
wren skills get generate-mdl # MDL from a database schema
|
||||
wren skills get dlt-connector # connect SaaS sources via dlt
|
||||
wren skills get enrich-context # add business context (units, enums, cubes)
|
||||
```
|
||||
|
||||
### `wren skills get <name> --full`
|
||||
|
||||
Include the skill's reference docs inline (sorted, separated). For skills
|
||||
that have no `references/`, the output is identical to the non-`--full` form.
|
||||
|
||||
### `wren skills get <name> --script <s>`
|
||||
|
||||
Print a bundled script's source to stdout. Currently:
|
||||
|
||||
```bash
|
||||
wren skills get dlt-connector --script introspect_dlt > introspect_dlt.py
|
||||
python introspect_dlt.py --duckdb-path ./pipeline.duckdb --output-dir ./project
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `wren ask` — Prompt Shaping
|
||||
|
||||
Wrap a natural-language question in one of two bundled templates and print
|
||||
the rendered prompt to stdout. **Does not execute any query** — it
|
||||
produces a prompt for an agent to consume.
|
||||
|
||||
You must explicitly pick one mode (no default — silently changing a
|
||||
default would alter agent behavior across an upgrade).
|
||||
|
||||
### `wren ask "<question>" --guided`
|
||||
|
||||
For weaker LLMs. Prepends a strict task flow (`wren context show` →
|
||||
`wren memory recall` → write SQL → `wren dry-plan` → `wren query`).
|
||||
|
||||
```bash
|
||||
wren ask "top 5 customers by revenue" --guided
|
||||
```
|
||||
|
||||
### `wren ask "<question>" --direct`
|
||||
|
||||
For stronger LLMs. Minimal wrapping; the agent decides which wren commands
|
||||
to run.
|
||||
|
||||
```bash
|
||||
wren ask "monthly orders trend" --direct
|
||||
```
|
||||
|
||||
+164
-76
@@ -1,48 +1,91 @@
|
||||
# Skills
|
||||
|
||||
Wren AI provides **skills** — reusable AI agent workflow guides that teach Claude Code (or other AI coding agents) how to use the Wren CLI effectively. Skills are not plugins or extensions; they are structured prompts with decision trees that guide an agent through multi-step tasks.
|
||||
Wren AI ships **skills** — reusable AI agent workflow guides that teach
|
||||
Claude Code (or other AI coding agents) how to use the Wren CLI for
|
||||
multi-step tasks. A skill is a structured markdown guide with a decision
|
||||
tree, agent-side rules, and references to deeper documentation.
|
||||
|
||||
## The new delivery model
|
||||
|
||||
Earlier versions of Wren shipped each skill as a separate folder of
|
||||
markdown installed into the agent's skill directory (`~/.claude/skills/`,
|
||||
Cursor `rules/`, etc.). That model had two recurring problems: the bundled
|
||||
markdown drifted from the installed CLI version, and the agent loaded all
|
||||
the content up-front whether it was needed or not.
|
||||
|
||||
Since Wren `0.8`, skill content **lives inside the `wren` CLI** and is
|
||||
served on demand:
|
||||
|
||||
- One ~50-line **discovery stub** is installed into your agent
|
||||
(`skills/wren/SKILL.md`). It teaches the agent that workflow guides and
|
||||
shaped prompts are fetched from the CLI.
|
||||
- The actual workflow guides live in the `wrenai` Python package and are
|
||||
printed to stdout by `wren skills get <name>`.
|
||||
- Prompt templates are served the same way: `wren ask "<q>" --guided|--direct`.
|
||||
Reference docs live on the web under
|
||||
[`docs/core/`](https://github.com/Canner/WrenAI/tree/main/docs/core).
|
||||
|
||||
Because the content travels with the wheel, the version the agent reads
|
||||
always matches the installed CLI.
|
||||
|
||||
## Available skills
|
||||
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| **wren-onboarding** | Entry point: environment checks, project scaffolding, profile setup, first query |
|
||||
| **wren-generate-mdl** | One-time setup: explore database schema, normalize types, scaffold MDL YAML project |
|
||||
| **wren-usage** | Day-to-day workflow: gather schema context, recall past queries, write SQL, execute, store results |
|
||||
| **wren-enrich-context** | Deepen business context the schema can't carry: enum/unit/null semantics, default filters, synonyms, currency rules, and named aggregation metrics as cubes — via grill or auto-pilot mode |
|
||||
| **wren-dlt-connector** | Connect SaaS APIs (HubSpot, Stripe, Salesforce, GitHub, Slack, …) into DuckDB via dlt, then auto-generate a Wren project |
|
||||
| Skill | Fetch with | Purpose |
|
||||
|-------|-----------|---------|
|
||||
| **onboarding** | `wren skills get onboarding` | Entry point: environment checks, project scaffolding, profile setup, first query |
|
||||
| **usage** | `wren skills get usage` | Day-to-day workflow: gather schema context, recall past queries, write SQL, execute, store results |
|
||||
| **generate-mdl** | `wren skills get generate-mdl` | One-time setup: explore database schema, normalize types, scaffold MDL YAML project |
|
||||
| **enrich-context** | `wren skills get enrich-context` | Deepen business context the schema can't carry: enum/unit/null semantics, default filters, synonyms, currency rules, and named aggregation metrics as cubes — via grill or auto-pilot mode |
|
||||
| **dlt-connector** | `wren skills get dlt-connector` | Connect SaaS APIs (HubSpot, Stripe, Salesforce, GitHub, Slack, …) into DuckDB via dlt, then auto-generate a Wren project |
|
||||
|
||||
List them with `wren skills list`.
|
||||
|
||||
## Installation
|
||||
|
||||
The installer supports every major AI coding agent (Claude Code, Openclaw, Hermes, Codex, etc.) and auto-detects which one you're using:
|
||||
|
||||
```bash
|
||||
# All skills at once
|
||||
npx skills add Canner/WrenAI --skill '*'
|
||||
|
||||
# Or via install script
|
||||
curl -fsSL https://raw.githubusercontent.com/Canner/WrenAI/main/skills/install.sh | bash
|
||||
pip install wrenai # core CLI (DuckDB included)
|
||||
npx skills add Canner/WrenAI # one-line discovery stub for your agent
|
||||
```
|
||||
|
||||
After installation, **start a new agent session** — skills are loaded at session start.
|
||||
The installer auto-detects Claude Code, Cursor, Cline, Codex, and similar
|
||||
clients. After installing, start a new agent session — the stub is loaded
|
||||
at session start; from then on it pulls workflow guides on demand.
|
||||
|
||||
### Update skills
|
||||
## How content is delivered
|
||||
|
||||
Skills check for updates automatically and notify the agent when a newer version is available. To force-update:
|
||||
The agent fetches guides using `wren skills get <name>`. The first call
|
||||
returns the SKILL.md body — a focused workflow guide. When the agent
|
||||
needs more depth it can ask for:
|
||||
|
||||
```bash
|
||||
# All skills
|
||||
curl -fsSL https://raw.githubusercontent.com/Canner/WrenAI/main/skills/install.sh | bash -s -- --force
|
||||
|
||||
# Single skill
|
||||
curl -fsSL https://raw.githubusercontent.com/Canner/WrenAI/main/skills/install.sh | bash -s -- --force wren-generate-mdl
|
||||
wren skills get <name> --full # include the skill's references/ inline
|
||||
wren skills get <name> --script <stem> # print a bundled script's source
|
||||
```
|
||||
|
||||
Shaped prompts are served the same way:
|
||||
|
||||
```bash
|
||||
wren ask "<question>" --guided # for weaker LLMs
|
||||
wren ask "<question>" --direct # for stronger LLMs
|
||||
```
|
||||
|
||||
Reference docs live on the web under
|
||||
[`docs/core/`](https://github.com/Canner/WrenAI/tree/main/docs/core).
|
||||
|
||||
A bundled CI guard (`tests/unit/test_served_content_guard.py`) scans every
|
||||
`wren <cmd>` invocation in served skill content, reference docs, and ask
|
||||
templates against the real CLI command tree — so a guide can't tell an
|
||||
agent to run a command or flag that doesn't exist.
|
||||
|
||||
---
|
||||
|
||||
## wren-onboarding
|
||||
## onboarding
|
||||
|
||||
The entry-point skill. It walks the agent through the full setup flow — environment checks, project scaffolding, connection configuration, MDL generation, and a first query — by routing to docs and other skills at each step. The skill itself stays focused on agent-side rules (one step per turn, never ask for credentials in chat).
|
||||
The entry-point skill. It walks the agent through the full setup flow —
|
||||
environment checks, project scaffolding, connection configuration, MDL
|
||||
generation, and a first query — by routing to docs and other skills at
|
||||
each step. The skill itself stays focused on agent-side rules (one step
|
||||
per turn, never ask for credentials in chat).
|
||||
|
||||
### Workflow
|
||||
|
||||
@@ -53,7 +96,7 @@ User says "install wren" / "set up wren"
|
||||
│ Python 3.11+, virtualenv, wren CLI, working dir
|
||||
│
|
||||
├── Branch: bundled demo or own database?
|
||||
│ demo → quickstart.md, stop
|
||||
│ demo → quickstart guide, stop
|
||||
│ own DB → continue
|
||||
│
|
||||
├── Step 1. Project name + database type
|
||||
@@ -70,7 +113,7 @@ User says "install wren" / "set up wren"
|
||||
│ wren profile debug
|
||||
│
|
||||
├── Step 5. Generate MDL
|
||||
│ dispatch → wren-generate-mdl skill
|
||||
│ wren skills get generate-mdl
|
||||
│
|
||||
└── Step 6. First query
|
||||
wren --sql "SELECT 1" (sanity)
|
||||
@@ -88,25 +131,25 @@ User says "install wren" / "set up wren"
|
||||
|
||||
### When to trigger
|
||||
|
||||
The skill activates on phrases like:
|
||||
The discovery stub routes the agent here on phrases like:
|
||||
|
||||
- "install wren"
|
||||
- "set up wren engine"
|
||||
- "connect a new database"
|
||||
- "I want to start a Wren project"
|
||||
- `/wren-onboarding`
|
||||
|
||||
### Reference docs (skill points to these, never duplicates)
|
||||
### Reference docs (the skill points to these, never duplicates)
|
||||
|
||||
- [Installation](../get_started/installation.md)
|
||||
- [Connect your database](/oss/guides/connect)
|
||||
- [Quickstart with sample data](../get_started/quickstart.md)
|
||||
- [`docs/core/get_started/installation.md`](https://github.com/Canner/WrenAI/blob/main/docs/core/get_started/installation.md)
|
||||
- [`docs/core/guides/connect.md`](https://github.com/Canner/WrenAI/blob/main/docs/core/guides/connect.md)
|
||||
- [`docs/core/get_started/quickstart.md`](https://github.com/Canner/WrenAI/blob/main/docs/core/get_started/quickstart.md)
|
||||
|
||||
---
|
||||
|
||||
## wren-usage
|
||||
## usage
|
||||
|
||||
The primary skill for day-to-day querying. It guides the agent through a complete query lifecycle.
|
||||
The primary skill for day-to-day querying. It guides the agent through a
|
||||
complete query lifecycle.
|
||||
|
||||
### Query workflow
|
||||
|
||||
@@ -141,7 +184,8 @@ The skill includes a two-layer error diagnosis strategy:
|
||||
| **MDL-level** | `wren dry-plan` fails | Wrong model/column names, missing relationships |
|
||||
| **DB-level** | `wren dry-plan` succeeds but execution fails | Type mismatch, permissions, dialect issues |
|
||||
|
||||
The agent checks `dry-plan` output first to isolate whether the error is in the semantic layer or the database.
|
||||
The agent checks `dry-plan` output first to isolate whether the error is
|
||||
in the semantic layer or the database.
|
||||
|
||||
### Additional workflows
|
||||
|
||||
@@ -152,16 +196,25 @@ The agent checks `dry-plan` output first to isolate whether the error is in the
|
||||
|
||||
### Reference files
|
||||
|
||||
The skill includes two reference documents loaded on demand:
|
||||
`wren skills get usage --full` inlines two reference documents:
|
||||
|
||||
- **memory.md** — Decision logic for when to `index`, `fetch`, `store`, and `recall`. Covers the hybrid retrieval strategy, store-by-default policy, and full lifecycle examples.
|
||||
- **wren-sql.md** — How the CTE-based rewrite pipeline works. Explains how the engine injects model CTEs, what SQL features are supported, and how to use `dry-plan` to diagnose errors layer by layer.
|
||||
- **memory.md** — decision logic for when to `index`, `fetch`, `store`,
|
||||
and `recall`. Covers the hybrid retrieval strategy, store-by-default
|
||||
policy, and full lifecycle examples.
|
||||
- **wren-sql.md** — how the CTE-based rewrite pipeline works. Explains
|
||||
how the engine injects model CTEs, what SQL features are supported,
|
||||
and how to use `dry-plan` to diagnose errors layer by layer.
|
||||
|
||||
---
|
||||
|
||||
## wren-enrich-context
|
||||
## enrich-context
|
||||
|
||||
The "enrich deep" companion to `wren-usage`. A schema-generated MDL only carries what the database can describe about itself — column names and types. The business meaning (what `status = 'A'` means, whether `amount` is in cents, which table is canonical, how the team defines ARR) lives in handbooks, glossaries, and analyst SQL. This skill brings that meaning into the project's reviewable context.
|
||||
The "enrich deep" companion to `usage`. A schema-generated MDL only
|
||||
carries what the database can describe about itself — column names and
|
||||
types. The business meaning (what `status = 'A'` means, whether `amount`
|
||||
is in cents, which table is canonical, how the team defines ARR) lives in
|
||||
handbooks, glossaries, and analyst SQL. This skill brings that meaning
|
||||
into the project's reviewable context.
|
||||
|
||||
### Two modes (chosen at session start)
|
||||
|
||||
@@ -170,11 +223,13 @@ The "enrich deep" companion to `wren-usage`. A schema-generated MDL only carries
|
||||
| **Grill** | Walks each gap one question at a time, proposes a concrete draft, waits for accept / edit / skip. May sample low-cardinality columns from the live DB (with your OK) to discover enum and sentinel values. | Sensitive data, or when you want to review every change |
|
||||
| **Auto-pilot** | Reads `raw/` + current context, applies its best inferences directly, escalates to grill only on raw-vs-MDL conflicts and high-blast-radius additions (new cubes / views / relationships). Hands you a confidence-tagged audit at the end. | Bulk backfill from a large doc set |
|
||||
|
||||
Both modes only **add** — they never modify an existing field. Contradictions are surfaced on a "please fix manually" list.
|
||||
Both modes only **add** — they never modify an existing field.
|
||||
Contradictions are surfaced on a "please fix manually" list.
|
||||
|
||||
### What it fills
|
||||
|
||||
The skill works from a ten-category gap catalog covering the business semantics a schema can't express:
|
||||
The skill works from a ten-category gap catalog covering the business
|
||||
semantics a schema can't express:
|
||||
|
||||
| Sink | Categories |
|
||||
|------|-----------|
|
||||
@@ -185,7 +240,7 @@ The skill works from a ten-category gap catalog covering the business semantics
|
||||
|
||||
### When to trigger
|
||||
|
||||
The skill activates on phrases like:
|
||||
The discovery stub routes the agent here on phrases like:
|
||||
|
||||
- "enrich context" / "augment my project" / "grill me on this project"
|
||||
- "the agent doesn't understand our docs / enum values / units"
|
||||
@@ -195,14 +250,20 @@ The skill activates on phrases like:
|
||||
|
||||
### Reference files
|
||||
|
||||
- **gap_catalog.md** — the ten gap categories with triggers, default sinks, and the prose-first `[tag]` write format.
|
||||
- **cube_proposals.md** — the decision tree for proposing a cube vs view vs calculated column, the cube YAML template, naming policy, duplication guard, and validation flow.
|
||||
`wren skills get enrich-context --full` inlines:
|
||||
|
||||
- **gap_catalog.md** — the ten gap categories with triggers, default
|
||||
sinks, and the prose-first `[tag]` write format.
|
||||
- **cube_proposals.md** — the decision tree for proposing a cube vs view
|
||||
vs calculated column, the cube YAML template, naming policy,
|
||||
duplication guard, and validation flow.
|
||||
|
||||
---
|
||||
|
||||
## wren-generate-mdl
|
||||
## generate-mdl
|
||||
|
||||
A one-time setup skill that walks the agent through creating an MDL project from a live database.
|
||||
A one-time setup skill that walks the agent through creating an MDL
|
||||
project from a live database.
|
||||
|
||||
### Seven-phase workflow
|
||||
|
||||
@@ -248,29 +309,47 @@ normalized = parse_type("character varying(255)", "postgres") # → "VARCHAR(25
|
||||
|
||||
---
|
||||
|
||||
## wren-dlt-connector
|
||||
## dlt-connector
|
||||
|
||||
A specialized skill for users who want to query SaaS data (HubSpot, Stripe, Salesforce, GitHub, Slack, …) with SQL. It chains a [dlt](https://dlthub.com) extraction pipeline into DuckDB with auto-generation of a Wren project on top.
|
||||
A specialized skill for users who want to query SaaS data (HubSpot,
|
||||
Stripe, Salesforce, GitHub, Slack, …) with SQL. It chains a
|
||||
[dlt](https://dlthub.com) extraction pipeline into DuckDB with
|
||||
auto-generation of a Wren project on top.
|
||||
|
||||
### Four-phase workflow
|
||||
|
||||
| Phase | Goal | Key actions |
|
||||
|-------|------|-------------|
|
||||
| **1. Extract** | Pull SaaS data into local DuckDB | `pip install "dlt[duckdb]"`, write a small `pipeline.py`, set source credentials, run `pipeline.run(source)` |
|
||||
| **2. Model** | Auto-generate a Wren project | Run `introspect_dlt.py` to scan DuckDB, normalize types via `wren.type_mapping.parse_type()`, write models, relationships, profile |
|
||||
| **2. Model** | Auto-generate a Wren project | Run the bundled `introspect_dlt` script to scan DuckDB, normalize types via `wren.type_mapping.parse_type()`, write models, relationships, profile |
|
||||
| **3. Build & Verify** | Confirm queries work end-to-end | `wren context build`, `wren memory index`, run sample SQL through the engine — not just file generation |
|
||||
| **4. Handoff** | Show first results | Run a couple of representative queries and surface them to the user |
|
||||
|
||||
The user can enter at any phase. If they already have a `.duckdb` file from a prior dlt run, the skill can start from Phase 2.
|
||||
The user can enter at any phase. If they already have a `.duckdb` file
|
||||
from a prior dlt run, the skill can start from Phase 2.
|
||||
|
||||
The introspection script is fetched separately rather than inlined:
|
||||
|
||||
```bash
|
||||
wren skills get dlt-connector --script introspect_dlt > introspect_dlt.py
|
||||
python introspect_dlt.py --duckdb-path ./pipeline.duckdb --output-dir ./project
|
||||
```
|
||||
|
||||
### Two non-negotiable invariants
|
||||
|
||||
1. **DuckDB catalog naming** — when Wren AI `ATTACH`es a `.duckdb` file, it uses the filename stem as the catalog alias. So every model's `table_reference.catalog` **must equal the filename stem**. `stripe_data.duckdb` → catalog `stripe_data`. The `introspect_dlt.py` script handles this automatically — never override.
|
||||
2. **Type normalization through wren SDK** — column types must go through `wren.type_mapping.parse_type()` (sqlglot-based). Don't hardcode mappings; DuckDB-specific types like `HUGEINT` or `TIMESTAMP WITH TIME ZONE` need canonical conversion.
|
||||
1. **DuckDB catalog naming** — when Wren AI `ATTACH`es a `.duckdb` file,
|
||||
it uses the filename stem as the catalog alias. So every model's
|
||||
`table_reference.catalog` **must equal the filename stem**.
|
||||
`stripe_data.duckdb` → catalog `stripe_data`. The bundled
|
||||
`introspect_dlt` script handles this automatically — never override.
|
||||
2. **Type normalization through wren SDK** — column types must go through
|
||||
`wren.type_mapping.parse_type()` (sqlglot-based). Don't hardcode
|
||||
mappings; DuckDB-specific types like `HUGEINT` or `TIMESTAMP WITH
|
||||
TIME ZONE` need canonical conversion.
|
||||
|
||||
### When to trigger
|
||||
|
||||
The skill activates on phrases like:
|
||||
The discovery stub routes the agent here on phrases like:
|
||||
|
||||
- "connect HubSpot / Stripe / Salesforce / GitHub / Slack data"
|
||||
- "load data from a SaaS API"
|
||||
@@ -280,36 +359,45 @@ The skill activates on phrases like:
|
||||
|
||||
### Source coverage
|
||||
|
||||
The skill ships a reference list of common dlt-verified sources with auth patterns. For sources not on the list, the agent checks [dlthub.com/docs/dlt-ecosystem/verified-sources](https://dlthub.com/docs/dlt-ecosystem/verified-sources) before improvising.
|
||||
The skill ships a reference list of common dlt-verified sources with auth
|
||||
patterns. For sources not on the list, the agent checks
|
||||
[dlthub.com/docs/dlt-ecosystem/verified-sources](https://dlthub.com/docs/dlt-ecosystem/verified-sources)
|
||||
before improvising.
|
||||
|
||||
---
|
||||
|
||||
## Skill structure
|
||||
## Skill bundle layout
|
||||
|
||||
Skills are installed to `~/.claude/skills/` with this layout:
|
||||
Inside the wheel, each skill is a directory under
|
||||
`src/wren/skills_content/`:
|
||||
|
||||
```text
|
||||
~/.claude/skills/
|
||||
├── wren-onboarding/
|
||||
│ └── SKILL.md # Setup workflow (routes to docs and other skills)
|
||||
├── wren-generate-mdl/
|
||||
│ └── SKILL.md # MDL generation workflow
|
||||
├── wren-usage/
|
||||
│ ├── SKILL.md # Day-to-day query workflow
|
||||
src/wren/skills_content/
|
||||
├── onboarding/
|
||||
│ └── SKILL.md
|
||||
├── usage/
|
||||
│ ├── SKILL.md
|
||||
│ └── references/
|
||||
│ ├── memory.md # Memory command decision logic
|
||||
│ └── wren-sql.md # CTE rewrite pipeline reference
|
||||
├── wren-enrich-context/
|
||||
│ ├── SKILL.md # Two-mode (grill / auto-pilot) context enrichment
|
||||
│ ├── memory.md
|
||||
│ └── wren-sql.md
|
||||
├── generate-mdl/
|
||||
│ └── SKILL.md
|
||||
├── enrich-context/
|
||||
│ ├── SKILL.md
|
||||
│ └── references/
|
||||
│ ├── gap_catalog.md # Ten business-semantic gap categories
|
||||
│ └── cube_proposals.md # When/how to propose cubes for aggregation metrics
|
||||
└── wren-dlt-connector/
|
||||
├── SKILL.md # SaaS-via-dlt → DuckDB → Wren project
|
||||
│ ├── gap_catalog.md
|
||||
│ └── cube_proposals.md
|
||||
└── dlt-connector/
|
||||
├── SKILL.md
|
||||
├── references/
|
||||
│ └── dlt_sources.md # Per-source dlt templates and auth patterns
|
||||
│ └── dlt_sources.md
|
||||
└── scripts/
|
||||
└── introspect_dlt.py # Auto-generates a Wren project from a .duckdb file
|
||||
└── introspect_dlt.py
|
||||
```
|
||||
|
||||
Each `SKILL.md` has YAML frontmatter with name, description, version, and license. The agent loads the main SKILL.md when triggered, and loads reference files or scripts on demand when deeper context is needed.
|
||||
Each `SKILL.md` has YAML frontmatter with name, description, and license.
|
||||
`wren skills get <name>` prints `SKILL.md`; `--full` appends every
|
||||
`references/*.md` in sorted order; `--script <stem>` prints a single file
|
||||
from `scripts/`. The skill bundle is shipped inside the `wrenai` wheel
|
||||
via Hatchling's `[tool.hatch.build.targets.wheel] artifacts` glob, so a
|
||||
fresh `pip install wrenai` always carries matching skill content.
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
"email": "dev@cannerdata.com"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Wren Engine CLI skills — semantic SQL, MDL generation, query workflows for 22+ data sources"
|
||||
"description": "Wren Engine — discovery stub for AI agents. Actual workflow guides and prompt helpers ship inside the wren CLI (pip install wrenai) and are served by `wren skills get` / `wren ask`."
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "wren",
|
||||
"source": ".",
|
||||
"description": "AI agent skills for Wren Engine CLI — semantic SQL layer for 22+ data sources.",
|
||||
"description": "Wren CLI for AI agents — semantic SQL over 22+ databases. Installs a single discovery stub that points the agent at the wren CLI for all workflow content.",
|
||||
"version": "1.0.0",
|
||||
"keywords": [
|
||||
"wren",
|
||||
|
||||
+60
-55
@@ -1,110 +1,115 @@
|
||||
# Skill Authoring Guide
|
||||
|
||||
Skills in this project follow the [Agent Skills](https://agentskills.io/) open format.
|
||||
Full specification: https://agentskills.io/specification
|
||||
> **Where to author new skill content:**
|
||||
> **NOT here under `skills/`.** New skill guides ship as Python package data
|
||||
> in [`core/wren/src/wren/skills_content/<name>/`](../core/wren/src/wren/skills_content/),
|
||||
> served at runtime by `wren skills get <name>`. That keeps the content
|
||||
> version-aligned with the installed `wren` CLI — no skill cache, no
|
||||
> `versions.json` drift hack.
|
||||
>
|
||||
> This `skills/` tree only contains the **distribution stubs** that AI clients
|
||||
> install (one new discovery stub `wren/`, plus five deprecated redirect
|
||||
> stubs kept for one release).
|
||||
|
||||
The skill content format below still follows the
|
||||
[Agent Skills](https://agentskills.io/) open spec — the only thing that
|
||||
changed is where the files live and how they reach the agent.
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
Each skill is a subdirectory containing a required `SKILL.md` and optional supporting directories:
|
||||
## Directory layout (under `core/wren/src/wren/skills_content/`)
|
||||
|
||||
```text
|
||||
skill-name/
|
||||
<name>/
|
||||
├── SKILL.md # Required — frontmatter + workflow instructions
|
||||
├── references/ # Optional — detail files loaded on demand
|
||||
├── references/ # Optional — reference docs, served by `wren skills get <name> --full`
|
||||
│ ├── some-topic.md
|
||||
│ └── another-topic.md
|
||||
└── scripts/ # Optional — executable scripts the agent can run
|
||||
└── scripts/ # Optional — bundled scripts, served by `wren skills get <name> --script <s>`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontmatter
|
||||
|
||||
Every `SKILL.md` must open with YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: skill-name
|
||||
description: "What this skill does and when to trigger it. Include specific
|
||||
trigger keywords. This field is loaded at startup for every conversation."
|
||||
name: <name> # matches the parent directory; lowercase, hyphens
|
||||
description: "What this skill does and when to trigger it. Include trigger
|
||||
keywords so an AI client can match it to user intent."
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: wrenai
|
||||
version: "1.0"
|
||||
---
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- `name` must exactly match the parent directory name (lowercase, hyphens only)
|
||||
- `description` is always loaded — keep it concise and keyword-rich so the agent can match it to user intent
|
||||
**Drop the `version:` field.** Content version = the installed wren-engine
|
||||
version, since the SKILL.md ships inside the wheel.
|
||||
|
||||
---
|
||||
|
||||
## Progressive Disclosure
|
||||
## Progressive disclosure
|
||||
|
||||
Skills load in three tiers. Design content for the tier where it is actually needed:
|
||||
|
||||
| Tier | Content | When loaded |
|
||||
| Tier | Content | Loaded when |
|
||||
|------|---------|-------------|
|
||||
| 1 — Metadata | `name` + `description` (~100 tokens) | Always, at every startup |
|
||||
| 2 — Instructions | Full `SKILL.md` body | When the skill is activated |
|
||||
| 3 — Resources | Files in `references/` or `scripts/` | Only when the agent explicitly reads them |
|
||||
| 1 — Discovery stub | `skills/wren/SKILL.md` frontmatter description (~few hundred tokens) | At every AI-client session start |
|
||||
| 2 — Main guide | `wren skills get <name>` | The agent runs the command when the user's task matches |
|
||||
| 3 — References | `wren skills get <name> --full` | The agent opts in when it needs depth |
|
||||
| 3 — Scripts | `wren skills get <name> --script <s>` | Same |
|
||||
|
||||
**Keep `SKILL.md` under 500 lines.** If the body is growing, move reference-only content to `references/`.
|
||||
Keep `SKILL.md` under ~500 lines. Move reference-only content into `references/`.
|
||||
|
||||
---
|
||||
|
||||
## What Goes Where
|
||||
## What goes where
|
||||
|
||||
### Keep in `SKILL.md`
|
||||
- Step-by-step workflow the agent follows
|
||||
- Decision criteria and branching logic
|
||||
- Short commands or invocations the agent needs immediately
|
||||
- Short commands the agent needs immediately
|
||||
- Quick reference tables (file paths, phase mappings, etc.)
|
||||
|
||||
### Move to `references/`
|
||||
Content that is only needed in certain code paths:
|
||||
- Output templates (report formats, plan file formats)
|
||||
- Per-case investigation details (e.g. per-stage debug steps)
|
||||
- Large lookup tables (connection info examples, error pattern catalogs)
|
||||
- Anything that would make `SKILL.md` exceed 300 lines
|
||||
- Per-case investigation details
|
||||
- Large lookup tables
|
||||
- Anything that would push `SKILL.md` past ~500 lines
|
||||
|
||||
Link to reference files from `SKILL.md` using paths relative to the skill root:
|
||||
Cross-link inside the same skill by reference name (delivered via `--full`):
|
||||
```markdown
|
||||
Follow [references/diagnose.md](references/diagnose.md) for per-stage investigation steps.
|
||||
For the CTE rewrite pipeline, see the `wren-sql` reference (run
|
||||
`wren skills get usage --full`).
|
||||
```
|
||||
|
||||
Cross-link to another skill via its CLI command:
|
||||
```markdown
|
||||
For day-to-day querying after setup, run `wren skills get usage`.
|
||||
```
|
||||
|
||||
Cross-link to a general doc:
|
||||
```markdown
|
||||
For troubleshooting, see [`connect.md`](https://github.com/Canner/WrenAI/blob/main/docs/core/guides/connect.md).
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions
|
||||
## Naming
|
||||
|
||||
| Item | Convention | Example |
|
||||
|------|-----------|---------|
|
||||
| Skill directory | `kebab-case` | `wren-generate-mdl/` |
|
||||
| `name` field | same as directory | `wren-generate-mdl` |
|
||||
|------|------------|---------|
|
||||
| Skill directory | `kebab-case` | `generate-mdl/` |
|
||||
| `name` field | same as directory | `generate-mdl` |
|
||||
| Reference files | descriptive `kebab-case` | `memory.md`, `wren-sql.md` |
|
||||
| Script files | descriptive `kebab-case` | `introspect_dlt.py` |
|
||||
|
||||
---
|
||||
|
||||
## Registration
|
||||
## Registering a new skill
|
||||
|
||||
After creating a new skill:
|
||||
1. Create the directory under `core/wren/src/wren/skills_content/<name>/` with `SKILL.md` (and optional `references/`, `scripts/`).
|
||||
2. Author the content following the format above.
|
||||
3. Add a test to `core/wren/tests/unit/test_skills_cli.py` verifying `wren skills get <name>` returns the guide and `--full` inlines its references (if any).
|
||||
4. Done — there is no separate `versions.json` / `index.json` to update; the package-data ships with the wheel and `wren skills list` enumerates it automatically.
|
||||
|
||||
1. Add a section to [SKILLS.md](SKILLS.md) describing the skill, its trigger conditions, and reference files.
|
||||
2. Add a row to the skills table in [README.md](README.md).
|
||||
3. Add the skill name and version to [versions.json](versions.json).
|
||||
4. Add an entry to [index.json](index.json) with `name`, `version`, `description`, `tags`, `dependencies` (if any), and `repository`.
|
||||
5. Add the skill to the `ALL_SKILLS` array in [install.sh](install.sh).
|
||||
|
||||
Both `versions.json` and `index.json` must stay in sync with the `version` field in the skill's `SKILL.md` frontmatter. Run `bash skills/check-versions.sh` to verify parity before merging.
|
||||
|
||||
---
|
||||
|
||||
## Releasing a skill update
|
||||
|
||||
1. Bump `version` in the skill's `SKILL.md` frontmatter.
|
||||
2. Update the matching version in `versions.json`.
|
||||
3. Update the matching version in `index.json`.
|
||||
4. Run `bash skills/check-versions.sh` — must pass before merging.
|
||||
No `bash skills/check-versions.sh` step — version drift is impossible by
|
||||
construction (content travels with the binary).
|
||||
|
||||
+35
-75
@@ -1,99 +1,59 @@
|
||||
# Wren Engine CLI Skills
|
||||
# Wren Engine — Agent Skill Distribution
|
||||
|
||||
This directory contains AI agent skills for working with the Wren Engine CLI (`wren`). Skills are instruction files that teach AI agents how to query data, generate MDL projects, and manage semantic layers using the `wren` CLI — no Docker or MCP server required.
|
||||
The actual skill content (workflow guides, reference docs, prompt helpers)
|
||||
**lives inside the `wren` CLI**. This directory ships a single discovery stub
|
||||
that an AI client installs once; the stub then tells the agent to fetch
|
||||
everything else from the CLI at runtime (so content always matches the
|
||||
installed wren-engine version).
|
||||
|
||||
## Installation
|
||||
See [`SKILLS.md`](SKILLS.md) for the full design and command surface.
|
||||
|
||||
### Option 1 — Claude Code Plugin
|
||||
## Install
|
||||
|
||||
Add the marketplace and install:
|
||||
### The CLI itself (where all skill content lives)
|
||||
|
||||
```bash
|
||||
pip install wrenai
|
||||
```
|
||||
|
||||
### The discovery stub (so an AI client knows the CLI exists)
|
||||
|
||||
#### Option 1 — Claude Code plugin
|
||||
|
||||
```text
|
||||
/plugin marketplace add Canner/WrenAI --path skills
|
||||
/plugin install wren@wren
|
||||
```
|
||||
|
||||
Or test locally during development:
|
||||
```bash
|
||||
claude --plugin-dir ./skills
|
||||
```
|
||||
|
||||
Skills are namespaced as `/wren:<skill>` (e.g., `/wren:wren-generate-mdl`, `/wren:wren-usage`).
|
||||
|
||||
### Option 2 — npx skills
|
||||
|
||||
Install all skills:
|
||||
```bash
|
||||
npx skills add Canner/WrenAI --skill '*'
|
||||
```
|
||||
|
||||
The CLI auto-detects your installed agent. To target a specific one, add `--agent <name>` (e.g., `claude-code`, `cursor`, `windsurf`, `cline`).
|
||||
|
||||
### Option 3 — install script (from a local clone)
|
||||
#### Option 2 — `npx skills`
|
||||
|
||||
```bash
|
||||
bash skills/install.sh # all skills
|
||||
bash skills/install.sh wren-usage # specific skill (auto-installs dependencies)
|
||||
bash skills/install.sh --force wren-usage # overwrite existing
|
||||
npx skills add Canner/WrenAI
|
||||
```
|
||||
|
||||
### Option 4 — manual copy
|
||||
The installer auto-detects your AI client. To target a specific one, add
|
||||
`--agent <name>` (e.g. `claude-code`, `cursor`, `windsurf`, `cline`).
|
||||
|
||||
#### Option 3 — local install script
|
||||
|
||||
```bash
|
||||
cp -r skills/wren-usage skills/wren-generate-mdl ~/.claude/skills/
|
||||
bash skills/install.sh # install the discovery stub
|
||||
bash skills/install.sh --force # overwrite existing
|
||||
```
|
||||
|
||||
Once installed, invoke a skill by name in your conversation:
|
||||
## What the agent does with the stub
|
||||
|
||||
```text
|
||||
/wren-usage
|
||||
/wren-generate-mdl
|
||||
```
|
||||
|
||||
> **Tip:** Use `--skill '*'` to install all skills at once, or specify individual skills.
|
||||
|
||||
## Available Skills
|
||||
|
||||
| Skill | Description |
|
||||
|-------|-------------|
|
||||
| [wren-usage](wren-usage/SKILL.md) | **Primary skill** — CLI workflow guide: query data via `wren --sql`, gather schema context with `wren memory`, store/recall queries, handle errors |
|
||||
| [wren-generate-mdl](wren-generate-mdl/SKILL.md) | Generate a Wren MDL project from a live database — schema discovery, type normalization, YAML generation |
|
||||
| [wren-dlt-connector](wren-dlt-connector/SKILL.md) | Connect SaaS data (HubSpot, Stripe, Salesforce, etc.) via dlt pipelines into DuckDB, then auto-generate a Wren project |
|
||||
|
||||
### wren-usage reference files
|
||||
|
||||
| File | Topic |
|
||||
|------|-------|
|
||||
| [references/memory.md](wren-usage/references/memory.md) | When to index, fetch, store, and recall |
|
||||
| [references/wren-sql.md](wren-usage/references/wren-sql.md) | CTE rewrite pipeline, SQL rules, error diagnosis |
|
||||
|
||||
## Updating Skills
|
||||
|
||||
Each skill automatically checks for updates when invoked. To update manually:
|
||||
Once installed, the agent reads `wren/SKILL.md` and learns to call:
|
||||
|
||||
```bash
|
||||
# Re-add to reinstall the latest version
|
||||
npx skills add Canner/WrenAI --skill '*'
|
||||
|
||||
# Or reinstall from a local clone
|
||||
bash skills/install.sh --force
|
||||
wren skills list # discover workflow guides
|
||||
wren skills get onboarding # fetch a guide (one of 5 names)
|
||||
wren docs connection-info <ds> # connection fields for a data source
|
||||
wren ask "<question>" --guided|--direct # wrap a prompt for an agent
|
||||
```
|
||||
|
||||
## Releasing a New Skill Version
|
||||
|
||||
When updating a skill, three files must be kept in sync:
|
||||
|
||||
1. Update `version` in the skill's `SKILL.md` frontmatter
|
||||
2. Update the matching entry in [`versions.json`](versions.json)
|
||||
3. Update the matching entry in [`index.json`](index.json)
|
||||
|
||||
Run `bash skills/check-versions.sh` to verify parity before merging.
|
||||
|
||||
## Requirements
|
||||
|
||||
- `wren` CLI installed (`pip install "wrenai"` or `pip install "wrenai[<datasource>]"`)
|
||||
- A database connection (configured via `wren profile add` or `~/.wren/connection_info.json`)
|
||||
- An AI client that supports skills (Claude Code, Cline, Cursor, etc.)
|
||||
|
||||
## Archived Skills (MCP-based)
|
||||
|
||||
The previous MCP server-based skills are preserved in [`skills-archive/`](../skills-archive/). Those skills require a running ibis-server and MCP server. The CLI skills in this directory replace that workflow with the standalone `wren` CLI.
|
||||
- `wren` CLI installed (`pip install wrenai` or `pip install "wrenai[<extras>]"`)
|
||||
- A database connection (configured via `wren profile add`)
|
||||
- An AI client that supports skills (Claude Code, Cursor, Cline, etc.)
|
||||
|
||||
+32
-122
@@ -1,132 +1,42 @@
|
||||
# Wren Engine CLI Skill Reference
|
||||
# Wren Engine — Agent Skills
|
||||
|
||||
Skills are instruction files that extend AI agents with Wren-specific workflows. Install them into your local skills folder and invoke them by name during a conversation.
|
||||
The actual workflow guides, reference docs, and prompt helpers live **inside
|
||||
the `wren` CLI itself**, so they always match the installed wren-engine
|
||||
version (no skill cache, no version drift).
|
||||
|
||||
---
|
||||
|
||||
## wren-usage
|
||||
|
||||
**File:** [wren-usage/SKILL.md](wren-usage/SKILL.md)
|
||||
|
||||
**Primary entry point** for day-to-day Wren Engine CLI usage. Covers the full query workflow: gather schema context, recall past queries, write SQL through the MDL semantic layer, execute via `wren --sql`, and store confirmed results.
|
||||
|
||||
### When to use
|
||||
|
||||
- Answering data questions using the `wren` CLI
|
||||
- Debugging SQL errors (MDL-level vs DB-level diagnosis)
|
||||
- Connecting a new data source via `wren profile`
|
||||
- Re-indexing memory after MDL changes
|
||||
- Any ongoing Wren task after initial setup is complete
|
||||
|
||||
### Reference files
|
||||
|
||||
| File | Topic |
|
||||
|------|-------|
|
||||
| [references/memory.md](wren-usage/references/memory.md) | When to index, fetch, store, and recall |
|
||||
| [references/wren-sql.md](wren-usage/references/wren-sql.md) | CTE rewrite pipeline, SQL rules, error diagnosis |
|
||||
|
||||
### Dependent skills
|
||||
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| `wren-generate-mdl` | Generate or regenerate MDL from a database |
|
||||
|
||||
---
|
||||
|
||||
## wren-generate-mdl
|
||||
|
||||
**File:** [wren-generate-mdl/SKILL.md](wren-generate-mdl/SKILL.md)
|
||||
|
||||
Generates a Wren MDL project by exploring a live database using whatever tools are available to the agent (SQLAlchemy, database drivers, raw SQL). Handles schema discovery, type normalization via `wren utils parse-type`, and YAML project scaffolding via `wren context init`.
|
||||
|
||||
### When to use
|
||||
|
||||
- Onboarding a new data source into Wren
|
||||
- Scaffolding an MDL project from an existing database schema
|
||||
- Re-generating models after database schema changes
|
||||
|
||||
### Workflow summary
|
||||
|
||||
1. Establish connection and agree on scope with the user
|
||||
2. Discover schema (tables, columns, types, constraints)
|
||||
3. Normalize types via `wren.type_mapping.parse_type` or `wren utils parse-type`
|
||||
4. Scaffold project with `wren context init`
|
||||
5. Write model YAML files and `relationships.yml`
|
||||
6. Validate (`wren context validate`) and build (`wren context build`)
|
||||
7. Initialize memory (`wren memory index`)
|
||||
|
||||
---
|
||||
|
||||
## wren-enrich-context
|
||||
|
||||
**File:** [wren-enrich-context/SKILL.md](wren-enrich-context/SKILL.md)
|
||||
|
||||
Augments a Wren project with the business context that DB schema cannot carry. The session starts by asking the user to pick one of two modes:
|
||||
|
||||
- **Grill mode** — one question at a time, agent proposes a draft, user accepts / edits / skips.
|
||||
- **Auto-pilot mode** — agent reads `raw/` + current context, applies best inferences directly, escalates to grill only on raw-vs-MDL conflicts and high-blast-radius additions (new metrics / views / relationships), and hands the user a confidence-tagged audit at the end.
|
||||
|
||||
Both modes read everything under `<project>/raw/` (PDFs, glossaries, handbooks, code, data dictionaries), compare against the current MDL / `instructions.md` / `queries.yml` / memory pairs, then fill missing relationships, metrics, views, default filters, business rules, and NL→SQL patterns. Confirmed findings are written back to the right sink — **only adds, never modifies existing fields**.
|
||||
|
||||
### When to use
|
||||
|
||||
- After scaffolding an MDL when the agent still doesn't grasp business semantics
|
||||
- When the user has handbooks / glossaries / financial reports / data dictionaries the agent should know
|
||||
- When schema-derived MDL is too thin to drive accurate SQL generation
|
||||
- When the user wants to commit project-wide rules (e.g., "user means type=default by default") into a place the agent will see them
|
||||
|
||||
### Sinks
|
||||
|
||||
| Sink | Type of finding |
|
||||
|------|-----------------|
|
||||
| MDL YAML | Schema structure, relationships, metrics, views, descriptions |
|
||||
| `instructions.md` | Default filters, implicit rules, business conventions |
|
||||
| `queries.yml` | Canonical NL→SQL pairs (git-trackable, team-shared) |
|
||||
| `wren memory store` (only when memory extra installed) | Ad-hoc user-local NL→SQL pairs |
|
||||
|
||||
### Dependent skills
|
||||
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| `wren-generate-mdl` | Generate the initial MDL before context augmentation |
|
||||
|
||||
---
|
||||
|
||||
## wren-dlt-connector
|
||||
|
||||
**File:** [wren-dlt-connector/SKILL.md](wren-dlt-connector/SKILL.md)
|
||||
|
||||
Connects SaaS data (HubSpot, Stripe, Salesforce, GitHub, Slack, etc.) to Wren Engine for SQL analysis. Walks through the full flow: install dlt, pick a SaaS source, set up credentials, run the data pipeline into DuckDB, then auto-generate a Wren semantic project from the loaded data.
|
||||
|
||||
### When to use
|
||||
|
||||
- Connecting SaaS data sources (HubSpot, Stripe, Salesforce, GitHub, Slack, etc.)
|
||||
- Importing data from an API via dlt pipelines
|
||||
- Loading SaaS data into DuckDB for SQL analysis
|
||||
- Creating a Wren project from an existing dlt-produced DuckDB file
|
||||
|
||||
### Dependent skills
|
||||
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| `wren-generate-mdl` | Generate or regenerate MDL from the DuckDB database |
|
||||
|
||||
---
|
||||
|
||||
## Installing a skill
|
||||
This directory ships a single discovery stub ([`wren/SKILL.md`](wren/SKILL.md))
|
||||
that an AI client can install. Once the agent reads the stub, it learns to
|
||||
fetch everything else from the CLI on demand:
|
||||
|
||||
```bash
|
||||
# Install wren-usage (auto-installs dependencies)
|
||||
bash skills/install.sh wren-usage
|
||||
wren skills list # all available workflow guides
|
||||
wren skills get <name> # fetch a guide
|
||||
wren skills get <name> --full # include the guide's reference docs
|
||||
wren skills get <name> --script <s> # fetch a bundled script
|
||||
|
||||
# Or install everything
|
||||
bash skills/install.sh
|
||||
wren docs connection-info <ds> # connection fields for a data source
|
||||
|
||||
wren ask "<question>" --guided # wrap a question for a weaker LLM
|
||||
wren ask "<question>" --direct # wrap a question for a stronger LLM
|
||||
```
|
||||
|
||||
Then invoke in your AI client:
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pip install wrenai # the CLI (everything is here)
|
||||
npx skills add Canner/WrenAI # install the discovery stub for AI clients
|
||||
```
|
||||
/wren-usage
|
||||
/wren-generate-mdl
|
||||
/wren-enrich-context
|
||||
|
||||
Or via Claude Code's plugin marketplace:
|
||||
|
||||
```text
|
||||
/plugin marketplace add Canner/WrenAI --path skills
|
||||
/plugin install wren@wren
|
||||
```
|
||||
|
||||
## Writing a new skill
|
||||
|
||||
New skill guides ship as Python package data in
|
||||
[`core/wren/src/wren/skills_content/<name>/`](../core/wren/src/wren/skills_content/),
|
||||
not as a new directory under this `skills/` tree. See
|
||||
[`AUTHORING.md`](AUTHORING.md).
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Verify that skills/versions.json and skills/index.json both match
|
||||
# the version in each skill's SKILL.md frontmatter.
|
||||
# Exits non-zero if any mismatch is found.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
VERSIONS_JSON="$SCRIPT_DIR/versions.json"
|
||||
INDEX_JSON="$SCRIPT_DIR/index.json"
|
||||
ERRORS=0
|
||||
|
||||
while IFS= read -r skill; do
|
||||
skill_name="${skill//\"/}"
|
||||
skill_name="${skill_name%%:*}"
|
||||
skill_name="${skill_name// /}"
|
||||
|
||||
versions_version=$(python3 -c "import json,sys; d=json.load(open('$VERSIONS_JSON')); print(d.get('$skill_name','MISSING'))")
|
||||
|
||||
skill_file="$SCRIPT_DIR/$skill_name/SKILL.md"
|
||||
if [ ! -f "$skill_file" ]; then
|
||||
echo "ERROR: $skill_name listed in versions.json but $skill_file not found" >&2
|
||||
ERRORS=$((ERRORS + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
md_version=$(grep -m1 'version:' "$skill_file" | sed 's/.*version: *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' "')
|
||||
|
||||
if [ "$versions_version" != "$md_version" ]; then
|
||||
echo "MISMATCH: $skill_name — versions.json=$versions_version, SKILL.md=$md_version" >&2
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "OK (versions.json): $skill_name @ $versions_version"
|
||||
fi
|
||||
|
||||
index_version=$(python3 -c "
|
||||
import json, sys
|
||||
skills = json.load(open('$INDEX_JSON')).get('skills', [])
|
||||
match = next((s['version'] for s in skills if s['name'] == '$skill_name'), 'MISSING')
|
||||
print(match)
|
||||
")
|
||||
|
||||
if [ "$index_version" != "$md_version" ]; then
|
||||
echo "MISMATCH: $skill_name — index.json=$index_version, SKILL.md=$md_version" >&2
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "OK (index.json): $skill_name @ $index_version"
|
||||
fi
|
||||
done < <(python3 -c "
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
root = Path('$SCRIPT_DIR')
|
||||
versions = set(json.load(open('$VERSIONS_JSON')).keys())
|
||||
index = {s['name'] for s in json.load(open('$INDEX_JSON')).get('skills', [])}
|
||||
skill_dirs = {p.parent.name for p in root.glob('*/SKILL.md')}
|
||||
|
||||
for name in sorted(versions | index | skill_dirs):
|
||||
print(name)
|
||||
")
|
||||
|
||||
if [ "$ERRORS" -gt 0 ]; then
|
||||
echo "" >&2
|
||||
echo "Found $ERRORS version mismatch(es). Update versions.json, index.json, or SKILL.md to match." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "All skill versions match."
|
||||
+5
-93
@@ -1,110 +1,22 @@
|
||||
{
|
||||
"name": "wrenai",
|
||||
"description": "AI agent skills for Wren Engine CLI — semantic SQL layer for 22+ data sources.",
|
||||
"description": "Discovery stub for the Wren AI agent skill bundle. The actual workflow guides and prompt helpers live inside the wren CLI (`pip install wrenai`) and are served by `wren skills get` / `wren ask`.",
|
||||
"homepage": "https://wren.ai",
|
||||
"repository": "https://github.com/Canner/WrenAI",
|
||||
"license": "Apache-2.0",
|
||||
"skills": [
|
||||
{
|
||||
"name": "wren-onboarding",
|
||||
"version": "2.2",
|
||||
"description": "Onboard a user to Wren Engine end-to-end. Walks the user through environment checks, .env configuration, connection profile creation, project scaffolding, binding the profile to the project, and first query. Defers procedural details, per-datasource notes, and the troubleshooting playbook to docs/core/guides/connect.md so the skill stays focused on agent-side rules and routing. Use when the user wants to install Wren Engine, set up a new data source connection, or bootstrap a new project from scratch.",
|
||||
"name": "wren",
|
||||
"description": "Wren CLI discovery stub. The actual workflow guides live inside the wren CLI; this stub teaches an AI agent to call `wren skills list`, `wren skills get <name>`, and `wren ask <prompt> --guided|--direct`. Use whenever the user asks a data question, wants to install / set up Wren Engine, connect a new database, load SaaS data via dlt, generate or regenerate an MDL project, or enrich a project with business context.",
|
||||
"tags": [
|
||||
"wren",
|
||||
"onboarding",
|
||||
"install",
|
||||
"setup",
|
||||
"discovery-stub",
|
||||
"cli",
|
||||
"dotenv",
|
||||
"profile"
|
||||
],
|
||||
"dependencies": [
|
||||
"wren-generate-mdl"
|
||||
],
|
||||
"repository": "https://github.com/Canner/WrenAI/tree/main/skills/wren-onboarding"
|
||||
},
|
||||
{
|
||||
"name": "wren-dlt-connector",
|
||||
"version": "1.0",
|
||||
"description": "Connect SaaS data (HubSpot, Stripe, Salesforce, GitHub, Slack, etc.) to Wren Engine for SQL analysis via dlt pipelines into DuckDB, then auto-generate a Wren semantic project.",
|
||||
"tags": [
|
||||
"wren",
|
||||
"dlt",
|
||||
"saas",
|
||||
"duckdb",
|
||||
"pipeline",
|
||||
"hubspot",
|
||||
"stripe",
|
||||
"salesforce",
|
||||
"github",
|
||||
"slack"
|
||||
],
|
||||
"dependencies": [
|
||||
"wren-generate-mdl"
|
||||
],
|
||||
"repository": "https://github.com/Canner/WrenAI/tree/main/skills/wren-dlt-connector"
|
||||
},
|
||||
{
|
||||
"name": "wren-generate-mdl",
|
||||
"version": "2.3",
|
||||
"description": "Generate a Wren MDL project by exploring a database with available tools (SQLAlchemy, database drivers, MCP connectors, or raw SQL). Guides agents through schema discovery, type normalization, and MDL YAML generation using the wren CLI.",
|
||||
"tags": [
|
||||
"wren",
|
||||
"mdl",
|
||||
"database",
|
||||
"introspection",
|
||||
"cli",
|
||||
"postgres",
|
||||
"bigquery",
|
||||
"snowflake",
|
||||
"mysql",
|
||||
"clickhouse",
|
||||
"trino"
|
||||
],
|
||||
"repository": "https://github.com/Canner/WrenAI/tree/main/skills/wren-generate-mdl"
|
||||
},
|
||||
{
|
||||
"name": "wren-usage",
|
||||
"version": "2.4",
|
||||
"description": "Wren Engine CLI workflow guide for AI agents. Triggers on data questions, reports, metrics, revenue, trends, 'how many', 'show me', 'top N', 'compare', 'breakdown'. Answer data questions end-to-end using the wren CLI.",
|
||||
"tags": [
|
||||
"wren",
|
||||
"usage",
|
||||
"sql",
|
||||
"mdl",
|
||||
"cli",
|
||||
"memory",
|
||||
"semantic-layer",
|
||||
"postgres",
|
||||
"bigquery",
|
||||
"snowflake"
|
||||
],
|
||||
"dependencies": [
|
||||
"wren-generate-mdl"
|
||||
],
|
||||
"repository": "https://github.com/Canner/WrenAI/tree/main/skills/wren-usage"
|
||||
},
|
||||
{
|
||||
"name": "wren-enrich-context",
|
||||
"version": "1.0",
|
||||
"description": "Augment a Wren project with business context that DB schema cannot carry — enum value meanings, units (USD vs cents, ms vs sec), NULL semantics, magic sentinels, soft-delete default filters, business synonyms, time-grain / TZ conventions, cross-system identifiers, currency rules, canonical-table preferences, and named aggregation metrics (ARR, churn, DAU) proposed as cubes. Two modes: `grill` (one question at a time) or `auto-pilot` (agent infers and applies, escalates on conflicts and high-blast-radius additions like new cubes / views / relationships). Reads raw/ (PDFs, glossaries, handbooks, code, data dictionaries) and optionally samples low-cardinality columns from the live DB (grill mode), compares against MDL / cubes / instructions.md / queries.yml / memory pairs, fills gaps via a ten-category gap catalog and a cube proposal flow. Only adds, never modifies existing fields.",
|
||||
"tags": [
|
||||
"wren",
|
||||
"context",
|
||||
"mdl",
|
||||
"enrich",
|
||||
"grill",
|
||||
"auto-pilot",
|
||||
"business-rules",
|
||||
"instructions",
|
||||
"queries",
|
||||
"memory",
|
||||
"semantic-layer"
|
||||
],
|
||||
"dependencies": [
|
||||
"wren-generate-mdl"
|
||||
],
|
||||
"repository": "https://github.com/Canner/WrenAI/tree/main/skills/wren-enrich-context"
|
||||
"repository": "https://github.com/Canner/WrenAI/tree/main/skills/wren"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+27
-145
@@ -1,46 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install Wren Engine CLI skills into your local AI agent skills directory.
|
||||
# Install the Wren AI agent skill discovery stub into your local AI client.
|
||||
#
|
||||
# The actual skill content lives inside the `wren` CLI itself
|
||||
# (`pip install wrenai`). This script installs the discovery stub (`wren`)
|
||||
# that points an AI client at the CLI; from then on the agent fetches
|
||||
# everything else via `wren skills get` / `wren ask`.
|
||||
#
|
||||
# Usage:
|
||||
# ./install.sh # install all skills
|
||||
# ./install.sh wren-usage # install specific skills
|
||||
# ./install.sh --force wren-usage # overwrite without prompt
|
||||
# ./install.sh # install the discovery stub
|
||||
# ./install.sh --force # overwrite an existing install
|
||||
# curl -fsSL https://raw.githubusercontent.com/Canner/WrenAI/main/skills/install.sh | bash
|
||||
# curl -fsSL .../install.sh | bash -s -- wren-generate-mdl
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO="Canner/WrenAI"
|
||||
BRANCH="${WREN_SKILLS_BRANCH:-main}"
|
||||
DEST="${CLAUDE_SKILLS_DIR:-$HOME/.claude/skills}"
|
||||
ALL_SKILLS=(wren-dlt-connector wren-enrich-context wren-generate-mdl wren-onboarding wren-usage)
|
||||
SKILL="wren"
|
||||
|
||||
# Parse --force flag and skill list from arguments
|
||||
FORCE=false
|
||||
SELECTED_SKILLS=()
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--force" ]; then
|
||||
FORCE=true
|
||||
else
|
||||
SELECTED_SKILLS+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${#SELECTED_SKILLS[@]}" -eq 0 ]; then
|
||||
SELECTED_SKILLS=("${ALL_SKILLS[@]}")
|
||||
fi
|
||||
|
||||
# Validate requested skills
|
||||
for skill in "${SELECTED_SKILLS[@]}"; do
|
||||
valid=false
|
||||
for known in "${ALL_SKILLS[@]}"; do
|
||||
if [ "$skill" = "$known" ]; then valid=true; break; fi
|
||||
done
|
||||
if [ "$valid" = false ]; then
|
||||
echo "Unknown skill: $skill" >&2
|
||||
echo "Available: ${ALL_SKILLS[*]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "$arg" in
|
||||
--force) FORCE=true ;;
|
||||
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Detect whether we are running from a local clone or piped via curl.
|
||||
@@ -49,139 +32,38 @@ if [ -n "${BASH_SOURCE[0]:-}" ] && [ "${BASH_SOURCE[0]}" != "/dev/stdin" ]; then
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
fi
|
||||
|
||||
# Locate index.json for dependency resolution (local or remote)
|
||||
INDEX_JSON=""
|
||||
INDEX_JSON_TMP=""
|
||||
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/index.json" ]; then
|
||||
INDEX_JSON="$SCRIPT_DIR/index.json"
|
||||
elif command -v curl &>/dev/null; then
|
||||
INDEX_JSON_TMP="$(mktemp)"
|
||||
if curl -fsSL "https://raw.githubusercontent.com/$REPO/$BRANCH/skills/index.json" -o "$INDEX_JSON_TMP" 2>/dev/null; then
|
||||
INDEX_JSON="$INDEX_JSON_TMP"
|
||||
else
|
||||
rm -f "$INDEX_JSON_TMP"
|
||||
INDEX_JSON_TMP=""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Expand SELECTED_SKILLS to include dependencies declared in index.json.
|
||||
expand_with_deps() {
|
||||
local json_file="$1"
|
||||
shift
|
||||
local -a input=("$@")
|
||||
local -a result=()
|
||||
|
||||
skill_in_result() {
|
||||
local s="$1"
|
||||
for r in "${result[@]:-}"; do [ "$r" = "$s" ] && return 0; done
|
||||
return 1
|
||||
}
|
||||
|
||||
is_known_skill() {
|
||||
local s="$1"
|
||||
for known in "${ALL_SKILLS[@]}"; do [ "$s" = "$known" ] && return 0; done
|
||||
return 1
|
||||
}
|
||||
|
||||
for skill in "${input[@]}"; do
|
||||
skill_in_result "$skill" || result+=("$skill")
|
||||
|
||||
if [ -n "$json_file" ] && command -v python3 &>/dev/null; then
|
||||
while IFS= read -r dep; do
|
||||
[ -z "$dep" ] && continue
|
||||
is_known_skill "$dep" || continue
|
||||
if ! skill_in_result "$dep"; then
|
||||
echo " + $dep (dependency of $skill)" >&2
|
||||
result+=("$dep")
|
||||
fi
|
||||
done < <(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(open(sys.argv[1]))
|
||||
s = next((x for x in d.get('skills', []) if x['name'] == sys.argv[2]), None)
|
||||
if s:
|
||||
for dep in s.get('dependencies', []):
|
||||
print(dep)
|
||||
except Exception:
|
||||
pass
|
||||
" "$json_file" "$skill" 2>/dev/null)
|
||||
fi
|
||||
done
|
||||
|
||||
printf '%s\n' "${result[@]}"
|
||||
}
|
||||
|
||||
# Only expand deps when installing specific skills (not the full set)
|
||||
if [ "${#SELECTED_SKILLS[@]}" -lt "${#ALL_SKILLS[@]}" ] && [ -n "$INDEX_JSON" ]; then
|
||||
EXPANDED=()
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] && EXPANDED+=("$line")
|
||||
done < <(expand_with_deps "$INDEX_JSON" "${SELECTED_SKILLS[@]}")
|
||||
SELECTED_SKILLS=("${EXPANDED[@]}")
|
||||
fi
|
||||
|
||||
install_from_local() {
|
||||
local src="$1" skill="$2" dest_dir="$3"
|
||||
install_skill() {
|
||||
local src="$1" dest_dir="$2"
|
||||
if [ "$FORCE" = false ] && [ -d "$dest_dir" ]; then
|
||||
echo " Skipping $skill (already exists). Use --force to overwrite."
|
||||
echo " Skipping $SKILL (already exists). Use --force to overwrite."
|
||||
return
|
||||
fi
|
||||
rm -rf "$dest_dir"
|
||||
cp -r "$src/$skill" "$dest_dir"
|
||||
echo " Installed $skill"
|
||||
}
|
||||
|
||||
install_from_archive() {
|
||||
local tmpdir="$1" skill="$2" dest_dir="$3"
|
||||
if [ "$FORCE" = false ] && [ -d "$dest_dir" ]; then
|
||||
echo " Skipping $skill (already exists). Use --force to overwrite."
|
||||
return
|
||||
fi
|
||||
if [ ! -d "$tmpdir/$skill" ]; then
|
||||
echo " Failed: $skill not found in archive" >&2
|
||||
return 1
|
||||
fi
|
||||
rm -rf "$dest_dir"
|
||||
cp -r "$tmpdir/$skill" "$dest_dir"
|
||||
echo " Installed $skill"
|
||||
cp -r "$src" "$dest_dir"
|
||||
echo " Installed $SKILL"
|
||||
}
|
||||
|
||||
mkdir -p "$DEST"
|
||||
|
||||
if [ -n "$SCRIPT_DIR" ] && [ -d "$SCRIPT_DIR/wren-generate-mdl" ]; then
|
||||
# ---- Local mode: copy directly from repo ----
|
||||
if [ -n "$SCRIPT_DIR" ] && [ -d "$SCRIPT_DIR/$SKILL" ]; then
|
||||
echo "Installing from local repo: $SCRIPT_DIR"
|
||||
echo "Destination: $DEST"
|
||||
echo ""
|
||||
for skill in "${SELECTED_SKILLS[@]}"; do
|
||||
install_from_local "$SCRIPT_DIR" "$skill" "$DEST/$skill"
|
||||
done
|
||||
install_skill "$SCRIPT_DIR/$SKILL" "$DEST/$SKILL"
|
||||
else
|
||||
# ---- Remote mode: download GitHub archive ----
|
||||
echo "Downloading skills from GitHub ($REPO @ $BRANCH)..."
|
||||
echo "Downloading skill from GitHub ($REPO @ $BRANCH)..."
|
||||
echo "Destination: $DEST"
|
||||
echo ""
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"; [ -n "${INDEX_JSON_TMP:-}" ] && rm -f "$INDEX_JSON_TMP"' EXIT
|
||||
|
||||
extract_paths=()
|
||||
for skill in "${SELECTED_SKILLS[@]}"; do
|
||||
extract_paths+=("WrenAI-${BRANCH}/skills/${skill}")
|
||||
done
|
||||
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
curl -fsSL "https://github.com/$REPO/archive/refs/heads/$BRANCH.tar.gz" \
|
||||
| tar -xz -C "$tmpdir" --strip-components=2 "${extract_paths[@]}"
|
||||
|
||||
for skill in "${SELECTED_SKILLS[@]}"; do
|
||||
install_from_archive "$tmpdir" "$skill" "$DEST/$skill"
|
||||
done
|
||||
| tar -xz -C "$tmpdir" --strip-components=2 "WrenAI-${BRANCH}/skills/${SKILL}"
|
||||
install_skill "$tmpdir/$SKILL" "$DEST/$SKILL"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Done. Invoke skills in your AI client:"
|
||||
for skill in "${SELECTED_SKILLS[@]}"; do
|
||||
echo " /$skill"
|
||||
done
|
||||
echo "Done. Invoke the skill in your AI client:"
|
||||
echo " /$SKILL"
|
||||
echo ""
|
||||
echo "To update skills later, re-run with --force:"
|
||||
echo "To update later, re-run with --force:"
|
||||
echo " curl -fsSL https://raw.githubusercontent.com/Canner/WrenAI/main/skills/install.sh | bash -s -- --force"
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"wren-dlt-connector": "1.0",
|
||||
"wren-generate-mdl": "2.3",
|
||||
"wren-enrich-context": "1.0",
|
||||
"wren-onboarding": "2.2",
|
||||
"wren-usage": "2.4"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"skill_name": "wren-dlt-connector",
|
||||
"evals": [
|
||||
{
|
||||
"id": 1,
|
||||
"prompt": "我想把公司 HubSpot CRM 的資料拉進來用 SQL 分析,可以幫我設定嗎?我有 HubSpot 的 private app token。",
|
||||
"expected_output": "Should guide through: install dlt, write a HubSpot pipeline script, set credentials, run pipeline, introspect DuckDB, generate wren project, build, and run sample queries on contacts/deals tables.",
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"prompt": "I already have a DuckDB file from a dlt pipeline at ./stripe_data.duckdb. I want to create a wren project so I can query the Stripe data with SQL. Can you set that up?",
|
||||
"expected_output": "Should skip Phase 1 (dlt setup), go directly to introspecting the DuckDB file, generate wren project YAML, set up profile, build, and run sample queries.",
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"prompt": "我們團隊用 GitHub 管理 open source project,我想定期把 issues 和 PR 資料抓下來做分析。怎麼開始?",
|
||||
"expected_output": "Should guide through: install dlt, configure GitHub access token, write pipeline for github source (issues + PRs), run into DuckDB, generate wren project with models for issues/pull_requests/comments, detect relationships, build, run sample queries.",
|
||||
"files": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: wren
|
||||
description: "Wren CLI for AI agents — a semantic SQL layer over 22+ databases (Postgres, MySQL, BigQuery, Snowflake, Spark, …). The actual workflow guides live inside the `wren` CLI itself; this is just a discovery stub. Use whenever the user asks a data question (how many, show me, top N, compare, trend, breakdown, metric, revenue, customers, orders), wants to install / set up Wren Engine, connect a new database, connect SaaS data via dlt (HubSpot, Stripe, Salesforce, GitHub, Slack), generate or regenerate an MDL project from a database schema, or enrich a project with business context (enum meanings, units, cubes like ARR / DAU / churn). Triggers: 'install wren', 'set up wren engine', 'connect database to wren', 'connect SaaS to wren', 'load hubspot / stripe / salesforce data', 'generate mdl', 'scaffold wren project', 'enrich wren context', 'augment my project', 'add cubes', 'wren onboarding', 'wren usage', 'wren generate mdl', 'wren dlt connector', 'wren enrich context'."
|
||||
license: Apache-2.0
|
||||
allowed-tools: Bash(wren:*)
|
||||
---
|
||||
|
||||
# Wren CLI
|
||||
|
||||
This is a discovery stub. The actual workflow guides and prompt helpers
|
||||
live inside the `wren` CLI itself, so they always match the installed
|
||||
wren-engine version (no skill cache, no version drift).
|
||||
|
||||
Install: `pip install wrenai`.
|
||||
|
||||
## Workflow guides
|
||||
|
||||
```bash
|
||||
wren skills list # all available workflow guides
|
||||
wren skills get onboarding # set up Wren end-to-end
|
||||
wren skills get usage # day-to-day querying
|
||||
wren skills get generate-mdl # generate MDL from a database schema
|
||||
wren skills get dlt-connector # connect SaaS sources via dlt
|
||||
wren skills get enrich-context # add business context (units, enums, cubes)
|
||||
# add --full to include the skill's reference docs
|
||||
# add --script <name> to fetch a bundled script (e.g. dlt-connector / introspect_dlt)
|
||||
```
|
||||
|
||||
## Reference docs
|
||||
|
||||
Full reference docs live on the web: <https://github.com/Canner/WrenAI/tree/main/docs/core>
|
||||
|
||||
```bash
|
||||
wren docs connection-info <ds> # required + optional connection fields for a data source
|
||||
```
|
||||
|
||||
## Prompt enhancement (wraps a user question for an agent)
|
||||
|
||||
```bash
|
||||
wren ask "<question>" --guided # for weaker LLMs (strict task flow)
|
||||
wren ask "<question>" --direct # for stronger LLMs (minimal wrapping)
|
||||
```
|
||||
|
||||
## Day-to-day data commands (not a sub-app — top-level)
|
||||
|
||||
```bash
|
||||
wren --sql '...' # execute SQL through the MDL layer
|
||||
wren query --sql '...' # same, explicit
|
||||
wren dry-plan --sql '...' # transpile only, no DB hit
|
||||
wren context show / build / validate # project / MDL lifecycle
|
||||
wren profile add / list / switch # named connection profiles
|
||||
wren memory index / recall / store # semantic memory (needs `[memory]` extra)
|
||||
```
|
||||
|
||||
Run `wren --help` for the full surface; load the matching `wren skills get
|
||||
<name>` guide before driving any multi-step workflow.
|
||||
Reference in New Issue
Block a user