Skip to content

outfitter-dev/trails

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

545 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Trails

Define once. Surface everywhere.

Trails is a contract-first TypeScript framework. Define a trail — typed input, Result output, examples, intent — and the framework projects it onto CLI, MCP, HTTP, or WebSocket. One definition, every surface, zero drift.

Trails ships CLI, MCP, and HTTP surfaces today. WebSocket is part of the architecture and roadmap, but not yet built.

Get started

With an AI agent

Claude Code — add the marketplace, then install the plugin:

claude plugin marketplace add outfitter-dev/trails
claude plugin install trails@trails

Codex, Cursor, and others — install the skill:

npx skills outfitter-dev/trails

The skill gives your agent the full Trails reference: lexicon, patterns, error taxonomy, surface wiring, testing, and before/after migration examples.

With code

bunx @ontrails/trails create

Follow the prompts — pick a name, choose a starter, select your surfaces. The scaffolder generates a working project with trails, a topo, surface wiring, and tests.

Or install manually:

bun add @ontrails/core @ontrails/cli @ontrails/commander zod
bun add -d @ontrails/testing

Before and after

Before: an Express handler

app.get('/api/projects/:id', async (req, res) => {
  try {
    const project = await db.projects.findById(req.params.id);
    if (!project) {
      return res.status(404).json({ error: 'Project not found' });
    }
    res.json(project);
  } catch (err) {
    console.error('Failed to fetch project:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

After: a trail

const show = trail('project.show', {
  input: z.object({ id: z.string().describe('Project ID') }),
  output: projectSchema,
  intent: 'read',
  examples: [
    { name: 'Found', input: { id: 'p_1' }, expected: { id: 'p_1', name: 'Acme' } },
    { name: 'Missing', input: { id: 'p_0' }, error: 'NotFoundError' },
  ],
  blaze: async (input) => {
    const project = await db.projects.findById(input.id);
    if (!project) return Result.err(new NotFoundError(`Project ${input.id} not found`));
    return Result.ok(project);
  },
});

Same logic. But now the framework derives:

  • CLI: myapp project show --id p_1 with --help text, exit code 2 for not-found
  • MCP: tool myapp_project_show with JSON Schema input, readOnlyHint annotation
  • Tests: both examples run as assertions — testAll(graph) validates the happy path and the error path
  • Governance: warden checks for throws, surface-specific imports in trail code, missing output schemas

You authored the contract. The framework did the rest.

What compounds

Each declaration you add to a trail unlocks derived behavior across every surface:

You add You get for free
input (Zod schema) CLI flags + --help text, MCP JSON Schema, input validation
output (Zod schema) Contract tests, MCP response typing, surface map entries
intent: 'read' MCP readOnlyHint, CLI skips confirmation, HTTP GET
intent: 'destroy' MCP destructiveHint, CLI auto-adds --dry-run, HTTP DELETE
examples Tests (happy + error path), agent guidance, documentation
crosses Composition graph, cycle detection, cross coverage in tests
resources: [db] Singleton lifecycle, test mock auto-resolution, warden governance
detours Recovery paths, detour contract validation, shadowing checks

The value isn't any single feature. It's that they multiply — each declaration makes every surface smarter without additional wiring.

How it works

import { trail, topo, Result } from '@ontrails/core';
import { surface as cliSurface } from '@ontrails/commander';
import { surface as mcpSurface } from '@ontrails/mcp';
import { z } from 'zod';

// 1. Define trails
const greet = trail('greet', {
  input: z.object({ name: z.string().describe('Who to greet') }),
  output: z.object({ message: z.string() }),
  intent: 'read',
  blaze: (input) => Result.ok({ message: `Hello, ${input.name}!` }),
});

// 2. Collect into topo
const graph = topo('myapp', { greet });

// 3. Open surfaces with any adapter
await cliSurface(graph);      // CLI
// await mcpSurface(graph);   // MCP — same trails, same run function

The same topo can be opened on HTTP today with @ontrails/hono. WebSocket follows the same peer-surface model, but is still planned.

$ myapp greet --name World
{ "message": "Hello, World!" }

Packages

Package What it does
@ontrails/core Result, errors, trail/signal/contour/topo, validation, schema derivation
@ontrails/cli CLI command model - flag derivation, output formatting
@ontrails/commander Commander adapter for the CLI surface
@ontrails/mcp MCP surface — tool generation, annotations, progress bridge
@ontrails/http HTTP surface model — route derivation, verb mapping, error responses
@ontrails/hono Hono adapter that opens a topo on the HTTP surface
@ontrails/store Backend-agnostic store definitions, typed accessors, adapter-support helpers
@ontrails/testing testAll(), testTrail(), testCrosses(), contract testing, surface harnesses
@ontrails/topographer Surface maps, semantic diffing, lock files for CI governance
@ontrails/observe Log and trace sink contracts, sink composition, built-in sinks, trace rendering
@ontrails/tracing Tracing compatibility, query/status trails, trails.db dev-state storage, sampling helpers, OTel adapter
@ontrails/logtape Adapter that forwards Trails log records to a LogTape-shaped logger
@ontrails/warden AST-based convention rules, drift detection, CI formatters

Documentation

See docs/index.md for the full guide, organized by what you're trying to do.

Development

bun run build          # Build all packages
bun run test           # Run all tests
bun run lint           # Lint with oxlint
bun run typecheck      # TypeScript strict mode

Status

v1 beta. The contract layer, CLI/MCP/HTTP surfaces, trails topo and trails dev workflows, shared trails.db, tracing-backed developer state, schema-derived stores, and the Drizzle runtime are implemented and shipping. The WebSocket surface is designed but not yet built. See Horizons for what's next.

About

Agent-native, contract-first TypeScript framework. Define once. Surface everywhere.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors