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:
Pin Hsu Chen
2026-06-04 09:49:35 +08:00
committed by GitHub
parent 731c9c0d92
commit cbd10cd0ae
38 changed files with 1482 additions and 920 deletions
-19
View File
@@ -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
+43 -37
View File
@@ -31,9 +31,9 @@
📺 HERO DEMO (place here)
─────────────────────────
Suggested: a 510 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.
+6 -1
View File
@@ -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
+33
View File
@@ -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)
+42
View File
@@ -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>
+6 -39
View File
@@ -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
+46
View File
@@ -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)
+66
View File
@@ -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)
@@ -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>
@@ -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()
@@ -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 515 **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.
@@ -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`
@@ -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 23 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 23 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`).
---
+141
View File
@@ -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")
)
+67
View File
@@ -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
+75
View File
@@ -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}"
)
+134
View File
@@ -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 == []
+74 -1
View File
@@ -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
View File
@@ -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.
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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).
-68
View File
@@ -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
View File
@@ -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
View File
@@ -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"
-7
View File
@@ -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": []
}
]
}
+56
View File
@@ -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.