Skip to content
76 changes: 73 additions & 3 deletions docs/docs/configure/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ Focus on the query: $ARGUMENTS

Skills are loaded from these locations (in priority order):

1. **altimate-code directories** (project-scoped, highest priority):
1. **Project directories** (project-scoped, highest priority):
- `.opencode/skills/`
- `.altimate-code/skill/`
- `.altimate-code/skills/`

Expand Down Expand Up @@ -88,9 +89,76 @@ altimate ships with built-in skills for common data engineering tasks. Type `/`
| `/train` | Learn standards from documents/style guides |
| `/training-status` | Dashboard of all learned knowledge |

## CLI Commands

Manage skills from the command line:

```bash
# List all skills with their paired CLI tools
altimate-code skill list

# List as JSON (for scripting)
altimate-code skill list --json

# Scaffold a new skill + CLI tool pair
altimate-code skill create my-tool
altimate-code skill create my-tool --language python
altimate-code skill create my-tool --language node
altimate-code skill create my-tool --skill-only # skill only, no CLI stub

# Validate a skill and its paired tool
altimate-code skill test my-tool
```

## Adding Custom Skills

Add your own skills as Markdown files in `.altimate-code/skill/`:
The fastest way to create a custom skill is with the scaffolder:

```bash
altimate-code skill create freshness-check
```

This creates two files:

- `.opencode/skills/freshness-check/SKILL.md` — teaches the agent when and how to use your tool
- `.opencode/tools/freshness-check` — executable CLI tool stub

### Pairing Skills with CLI Tools

Skills become powerful when paired with CLI tools. Drop any executable into `.opencode/tools/` and it's automatically available on the agent's PATH:

```
.opencode/tools/ # Project-level tools (auto-discovered)
~/.config/altimate-code/tools/ # Global tools (shared across projects)
```
Comment on lines +130 to +133
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a language to this fenced block.

markdownlint is already flagging this fence. Mark it as text (or the appropriate language) so the docs stay lint-clean.

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 130-130: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/docs/configure/skills.md` around lines 130 - 133, The fenced code block
containing the two paths (".opencode/tools/           # Project-level tools
(auto-discovered" and "~/.config/altimate-code/tools/  # Global tools (shared
across projects)") needs an explicit language to satisfy markdownlint; change
the opening fence from ``` to ```text (or another appropriate language) so the
block is annotated (e.g., use ```text) and save the change in the
docs/docs/configure/skills.md file where that fenced block appears.


A skill references its paired CLI tool through bash code blocks:

```markdown
---
name: freshness-check
description: Check data freshness across tables
---

# Freshness Check

## CLI Reference
\`\`\`bash
freshness-check --table users --threshold 24h
freshness-check --all --report
\`\`\`

## Workflow
1. Ask the user which tables to check
2. Run `freshness-check` with appropriate flags
3. Interpret the output and suggest fixes
```

The tool can be written in any language (bash, Python, Node.js, etc.) — as long as it's executable.

### Skill-Only (No CLI Tool)

You can also create skills as plain prompt templates:

```markdown
---
Expand All @@ -104,9 +172,11 @@ Focus on: $ARGUMENTS

`$ARGUMENTS` is replaced with whatever the user types after the skill name (e.g., `/cost-review SELECT * FROM orders` passes `SELECT * FROM orders`).

### Skill Paths

Skills are loaded from these paths (highest priority first):

1. `.altimate-code/skill/` (project)
1. `.opencode/skills/` and `.altimate-code/skill/` (project)
2. `~/.altimate-code/skills/` (global)
3. Custom paths via config:

Expand Down
68 changes: 66 additions & 2 deletions docs/docs/configure/tools/custom.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,72 @@
# Custom Tools

Create custom tools using TypeScript and the altimate plugin system.
There are two ways to extend altimate-code with custom tools:

## Quick Start
1. **CLI tools** (recommended) — simple executables paired with skills
2. **Plugin tools** — TypeScript-based tools using the plugin API

## CLI Tools (Recommended)

The simplest way to add custom functionality. Drop any executable into `.opencode/tools/` and it's automatically available to the agent via bash.

### Quick Start

```bash
# Scaffold a skill + CLI tool pair
altimate-code skill create my-tool

# Or create manually:
mkdir -p .opencode/tools
cat > .opencode/tools/my-tool << 'EOF'
#!/usr/bin/env bash
set -euo pipefail
echo "Hello from my-tool!"
EOF
chmod +x .opencode/tools/my-tool
```

Tools in `.opencode/tools/` are automatically prepended to PATH when the agent runs bash commands. No configuration needed.

### Tool Locations

| Location | Scope | Auto-discovered |
|----------|-------|-----------------|
| `.opencode/tools/` | Project | Yes |
| `~/.config/altimate-code/tools/` | Global (all projects) | Yes |

### Pairing with Skills

Create a `SKILL.md` that teaches the agent when and how to use your tool:

```bash
altimate-code skill create my-tool --language python
```

This creates both `.opencode/skills/my-tool/SKILL.md` and `.opencode/tools/my-tool`. Edit both files to implement your tool.

### Validating

```bash
altimate-code skill test my-tool
```

This checks that the SKILL.md is valid and the paired tool is executable.

### Output Conventions

For best results with the AI agent:

- **Default output:** Human-readable text (the agent reads this well)
- **`--json` flag:** Structured JSON for scripting
- **Summary first:** "Found 12 matches:" or "3 issues detected:"
- **Errors to stderr**, results to stdout
- **Exit code 0** = success, **1** = error

## Plugin Tools (Advanced)

For more complex tools that need access to the altimate-code runtime, use the TypeScript plugin system.

### Quick Start

1. Create a tools directory:

Expand Down
108 changes: 108 additions & 0 deletions packages/opencode/src/cli/cmd/skill-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// altimate_change start — shared helpers for skill CLI commands
import path from "path"
import fs from "fs/promises"
import { Global } from "@/global"
import { Instance } from "../../project/instance"

/** Shell builtins, common utilities, and agent tool names to filter when detecting CLI tool references. */
export const SHELL_BUILTINS = new Set([
// Shell builtins
"echo", "cd", "export", "set", "if", "then", "else", "fi", "for", "do", "done",
"case", "esac", "printf", "source", "alias", "read", "local", "return", "exit",
"break", "continue", "shift", "trap", "type", "command", "builtin", "eval", "exec",
"test", "true", "false",
// Common CLI utilities (not user tools)
"cat", "grep", "awk", "sed", "rm", "cp", "mv", "mkdir", "ls", "chmod", "which",
"curl", "wget", "pwd", "touch", "head", "tail", "sort", "uniq", "wc", "tee",
"xargs", "find", "tar", "gzip", "unzip", "git", "npm", "yarn", "bun", "pip",
"python", "python3", "node", "bash", "sh", "zsh", "docker", "make",
// System utilities unlikely to be user tools
"sudo", "kill", "ps", "env", "whoami", "id", "date", "sleep", "diff", "less", "more",
// Agent tool names that appear in skill content but aren't CLI tools
"glob", "write", "edit",
])

/** Detect CLI tool references inside a skill's content (bash code blocks mentioning executables). */
export function detectToolReferences(content: string): string[] {
const tools = new Set<string>()

// Match "Tools used: bash (runs `altimate-dbt` commands), ..."
const toolsUsedMatch = content.match(/Tools used:\s*(.+)/i)
if (toolsUsedMatch) {
const refs = toolsUsedMatch[1].matchAll(/`([a-z][\w-]*)`/gi)
for (const m of refs) {
if (!SHELL_BUILTINS.has(m[1])) tools.add(m[1])
}
}

// Match executable names in bash code blocks: lines starting with an executable name
const bashBlocks = content.matchAll(/```(?:bash|sh)\r?\n([\s\S]*?)```/g)
for (const block of bashBlocks) {
const lines = block[1].split("\n")
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith("#")) continue
// Extract the first word (the command)
const cmdMatch = trimmed.match(/^(?:\$\s+)?([a-z][\w.-]*(?:-[\w]+)*)/i)
if (cmdMatch) {
const cmd = cmdMatch[1]
if (!SHELL_BUILTINS.has(cmd)) {
tools.add(cmd)
}
}
}
}

return Array.from(tools)
}

/** Determine the source label for a skill based on its location. */
export function skillSource(location: string): string {
if (location.startsWith("builtin:")) return "builtin"
const home = Global.Path.home
// Builtin skills shipped with altimate-code
if (location.startsWith(path.join(home, ".altimate", "builtin"))) return "builtin"
// Global user skills (~/.claude/skills/, ~/.agents/skills/, ~/.config/altimate-code/skills/)
const globalDirs = [
path.join(home, ".claude", "skills"),
path.join(home, ".agents", "skills"),
path.join(home, ".altimate-code", "skills"),
path.join(Global.Path.config, "skills"),
]
if (globalDirs.some((dir) => location.startsWith(dir))) return "global"
// Everything else is project-level
return "project"
}

/** Check if a tool is available on the current PATH (including .opencode/tools/). */
export async function isToolOnPath(toolName: string, cwd: string): Promise<boolean> {
// Check .opencode/tools/ in both cwd and worktree (they may differ in monorepos)
const dirsToCheck = new Set([
path.join(cwd, ".opencode", "tools"),
path.join(Instance.worktree !== "/" ? Instance.worktree : cwd, ".opencode", "tools"),
path.join(Global.Path.config, "tools"),
])

for (const dir of dirsToCheck) {
try {
await fs.access(path.join(dir, toolName), fs.constants.X_OK)
return true
} catch {}
}

// Check system PATH
const sep = process.platform === "win32" ? ";" : ":"
const binDir = process.env.ALTIMATE_BIN_DIR
const pathDirs = (process.env.PATH ?? "").split(sep).filter(Boolean)
if (binDir) pathDirs.unshift(binDir)

for (const dir of pathDirs) {
try {
await fs.access(path.join(dir, toolName), fs.constants.X_OK)
return true
} catch {}
}

return false
}
// altimate_change end
Loading
Loading