core

package
v0.9.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 26, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package core — atomic.go: shared file-mutation primitives for Edit and Write. These helpers guarantee no partial-write artifacts on crash and give us a single place to add safety polish (line-ending preservation, BOM handling) consistently across both tools.

Per ADR-007 we do not use a third-party "atomic write" library — Go's stdlib (`os.WriteFile` + `os.Rename`) is enough when used correctly. The platform invariant we rely on: rename(2) on the same filesystem is atomic, so writers never see a half-written file at the target path.

Package core implements clawtool's canonical core tools per ADR-005.

Quality bar reminder: each core tool must measurably beat the corresponding native built-in across the major agents. For Bash:

  • timeout-safe: output is captured even when the process is killed
  • predictable cwd: defaults to $HOME, never the daemon's cwd
  • structured result: stdout/stderr/exit_code/duration_ms/timed_out returned as JSON even on timeout

v0.1 implements timeout-safe + structured result + cwd. Secret redaction and per-session command history land in v0.2.

Package core — Edit performs a precise search-and-replace on an existing file with safety polish:

  • uniqueness check by default (refuses ambiguous edits)
  • atomic temp+rename so a crash never leaves a half-written file
  • line-ending preserve (LF / CRLF / CR detected from current content)
  • BOM preserve
  • binary refusal (symmetric with Read)

Per ADR-007 we wrap stdlib `os` for I/O and add our own polish layer. The search-and-replace shape mirrors what Claude Code's native Edit uses today (old_string / new_string / replace_all) — agents that learnt that interface get the same affordances against clawtool's stronger invariants.

Package core — Glob is the canonical wrapper around bmatcuk/doublestar.

Per ADR-007 we don't write a glob engine. doublestar is the de-facto double-star (`**`) glob library in Go, used by GoReleaser, k6, etc. This file's value is the polish layer: cwd-aware path resolution, uniform structured output, hard cap to protect agent context, and platform-stable separators (the wrapper always returns forward-slash paths regardless of OS — agents expect that).

Package core — Grep wraps ripgrep when present, falls back to system grep.

Per ADR-007 we curate ripgrep as the default engine: it has the correct .gitignore semantics, --type aliases, fast Aho-Corasick matching, and a stable JSON output that is straightforward to parse. The fallback is system grep (-rn) so clawtool's Grep is never unavailable.

Output is uniform across engines so the agent never has to know which one ran. The `engine` field in the result lets users / tests verify.

Package core — Read returns file content with stable line cursors, deterministic line counts, and per-format dispatch.

Per ADR-007 we wrap mature engines instead of writing parsers:

  • text → stdlib bufio (line-walked, single-pass)
  • pdf → pdftotext (poppler-utils) shell-out
  • ipynb → native JSON cell parse
  • docx → pandoc shell-out (universal office converter)
  • xlsx → github.com/xuri/excelize/v2 (Microsoft/Alibaba/Oracle in prod)
  • csv / tsv → stdlib encoding/csv (header + bounded preview)
  • html → github.com/go-shiori/go-readability (Mozilla Readability port)
  • json / yaml / toml / xml → text passthrough with format tag
  • binary → refused with structured error

Adding a new format means: extend detectFormat, add a reader function, update CoreToolDocs's description, ship tests for the format. The readResult shape stays uniform so the agent never has to branch on which engine ran.

Package core — Recipe* MCP tools mirror the `clawtool recipe …` CLI surface so a model can run setup tasks from inside a conversation. Same registry, same Detect/Apply/Verify cycle.

Three tools per ADR-013:

RecipeList   — enumerate recipes, optionally filter by category.
RecipeStatus — Detect output for one recipe or all of them.
RecipeApply  — full Detect→Prereqs→Apply→Verify against a repo,
               with structured options.

All three return BaseResult-shaped output (pretty text + structured JSON) so chat UIs render them the same way as Bash/Read/etc.

Package core — SkillNew MCP tool. Mirrors `clawtool skill new` so a model can scaffold a Claude Code skill from inside a conversation, without the user dropping to a shell. Both surfaces share the same template renderer (passed in at registration so we don't reach across packages — this file stays a leaf).

Spec compliance: agentskills.io. SKILL.md gets YAML frontmatter with `name` + `description` (required) plus optional `triggers`, followed by the body template. Same shape as the CLI emits.

Package core — ToolSearch is the discovery primitive that makes a 50+ tool catalog usable per ADR-005. The agent calls ToolSearch with a natural-language query, gets ranked candidates, then binds to the right tool with its full schema via the regular tools/list output.

Package core — WebFetch retrieves a URL and renders the body in the best shape for an AI agent: clean article text for HTML, raw text for plain-text MIME types, structured rejection for binaries.

Per ADR-007 we wrap two mature engines:

  • net/http stdlib client for transport (proxy, TLS, redirect handling are all stock — battle-tested across Go's user base);
  • github.com/go-shiori/go-readability for HTML extraction (Mozilla Readability port, the same algorithm Firefox Reader View ships).

What clawtool adds: agent-friendly polish — a hard body cap so a runaway page can't blow context, structured result with content-type-aware `format`, citation metadata (final URL after redirects, fetched_at).

Package core — WebSearch is a pluggable web-search primitive. clawtool itself does no crawling or ranking; it adapts whichever search backend the user has configured (Brave today; Tavily / SearXNG planned) into a uniform `{results, backend, …}` shape.

Per ADR-007 we wrap, never reinvent. The backend interface is small on purpose so adding a new provider is one file (see websearch_brave.go for the canonical example).

Package core — Write creates or overwrites a whole file with given content. Sister tool to Edit; the boundary is intentional:

  • Edit modifies a substring of an existing file. Refuses if the file is missing.
  • Write replaces or creates the whole file. If the file already exists and has detectable line endings, those endings are preserved by default so existing tooling (CR LF on Windows, etc.) keeps working.

Per ADR-007 we wrap stdlib `os` for I/O. The polish layer is the same atomic-write primitive Edit uses, plus a parent-directory auto-create so agents don't need a separate `mkdir` step before writing.

Index

Constants

This section is empty.

Variables

View Source
var ErrMissingAPIKey = errors.New("missing API key")

ErrMissingAPIKey is returned by backends when their required API key is not present in either the secrets store or process env.

Functions

func CoreToolDocs

func CoreToolDocs() []search.Doc

CoreToolDocs returns search.Doc descriptors for every clawtool core tool. Centralised so the index-builder in server/server.go stays a one-liner and there's a single source of truth for what each core tool's description says — same string the user sees in tools/list.

func RegisterBash

func RegisterBash(s *server.MCPServer)

RegisterBash adds the Bash tool to the given MCP server.

func RegisterEdit

func RegisterEdit(s *server.MCPServer)

RegisterEdit adds the Edit tool to the given MCP server.

func RegisterGlob

func RegisterGlob(s *server.MCPServer)

RegisterGlob adds the Glob tool to the given MCP server.

func RegisterGrep

func RegisterGrep(s *server.MCPServer)

RegisterGrep adds the Grep tool to the given MCP server.

func RegisterRead

func RegisterRead(s *server.MCPServer)

RegisterRead adds the Read tool to the given MCP server.

func RegisterRecipeTools added in v0.9.0

func RegisterRecipeTools(s *server.MCPServer)

RegisterRecipeTools adds the three Recipe* MCP tools to s. The registry must already be populated (typically via blank import of internal/setup/recipes).

func RegisterSkillNew added in v0.9.0

func RegisterSkillNew(s *server.MCPServer)

RegisterSkillNew adds the SkillNew tool to s. Template + helpers come from internal/skillgen so this MCP surface and the `clawtool skill new` CLI emit byte-identical output.

func RegisterToolSearch

func RegisterToolSearch(s *server.MCPServer, idx *search.Index)

RegisterToolSearch adds the ToolSearch tool to the given MCP server, closing over the index built at startup.

func RegisterWebFetch

func RegisterWebFetch(s *server.MCPServer)

RegisterWebFetch adds the WebFetch tool to the given MCP server.

func RegisterWebSearch

func RegisterWebSearch(s *server.MCPServer, store *secrets.Store)

RegisterWebSearch adds the WebSearch tool to the given MCP server. The secrets-store reference is captured so per-call backend resolution can pick up updated API keys without restart.

func RegisterWrite

func RegisterWrite(s *server.MCPServer)

RegisterWrite adds the Write tool to the given MCP server.

func ResetEngineCache

func ResetEngineCache()

ResetEngineCache forces a re-detection on next LookupEngine call. Used by tests that manipulate $PATH.

Types

type Backend

type Backend interface {
	Name() string
	Search(ctx context.Context, query string, limit int) ([]WebSearchHit, error)
}

Backend abstracts a web-search provider. Implementations must be safe to invoke from multiple goroutines and complete within the supplied context's deadline.

type BaseResult added in v0.9.0

type BaseResult struct {
	Operation   string `json:"-"`
	Engine      string `json:"engine,omitempty"`
	DurationMs  int64  `json:"duration_ms,omitempty"`
	ErrorReason string `json:"error_reason,omitempty"`
}

BaseResult holds the fields and rendering helpers every tool result shares. Each tool's *Result struct embeds this so the JSON shape stays uniform (timing, error, engine surfaced the same way everywhere) and Render() implementations stay short — each one is just "header + body + footer" composed from BaseResult helpers and the tool-specific data.

Operation is JSON-omitted; it's a presentation concern (the header verb), not a wire field.

func (BaseResult) ErrorLine added in v0.9.0

func (b BaseResult) ErrorLine(target string) string

ErrorLine renders the canonical failure one-liner. Every tool that fails uses this — keeps "✗ <verb> — <reason>" consistent across the whole catalog.

func (BaseResult) FooterLine added in v0.9.0

func (b BaseResult) FooterLine(extras ...string) string

FooterLine joins extras with " · " and appends the duration. Used at the bottom of multi-line results (after content).

func (BaseResult) HeaderLine added in v0.9.0

func (b BaseResult) HeaderLine(title string) string

HeaderLine renders the canonical multi-line header used by tools that return content (Bash, Read, Grep). Engine — when set — is shown in brackets so the caller always knows which backend ran.

func (BaseResult) IsError added in v0.9.0

func (b BaseResult) IsError() bool

IsError is the universal failure predicate.

func (BaseResult) SuccessLine added in v0.9.0

func (b BaseResult) SuccessLine(target string, extras ...string) string

SuccessLine is the canonical single-line success format used by stateless tools (Edit, Write). Variadic extras are joined with " · " and the duration is appended automatically.

type EditResult

type EditResult struct {
	BaseResult
	Path                string `json:"path"`
	Replaced            bool   `json:"replaced"`
	OccurrencesReplaced int    `json:"occurrences_replaced"`
	SizeBytesBefore     int64  `json:"size_bytes_before"`
	SizeBytesAfter      int64  `json:"size_bytes_after"`
	LineEndings         string `json:"line_endings"`
}

EditResult is the uniform shape returned to the agent.

func (EditResult) Render added in v0.9.0

func (r EditResult) Render() string

Render satisfies the Renderer contract. Single-line success/failure; stateless tools don't need a multi-line body.

type Engine

type Engine struct {
	Name string // canonical name, e.g. "ripgrep"
	Bin  string // resolved absolute path, empty if absent
}

Engine describes one detected upstream binary that clawtool wraps.

Per ADR-007, clawtool curates and wraps best-in-class engines. Detection runs once on startup so wrapper tools can pick the best available engine without re-shelling-out for every call.

func LookupEngine

func LookupEngine(name string) Engine

LookupEngine returns the cached engine entry for a given binary name. The Bin field is empty if the engine is not present on this system.

type GlobResult

type GlobResult struct {
	BaseResult
	Matches      []string `json:"matches"`
	MatchesCount int      `json:"matches_count"`
	Truncated    bool     `json:"truncated"`
	Cwd          string   `json:"cwd"`
	Pattern      string   `json:"pattern"`
}

GlobResult is the uniform shape returned to the agent.

func (GlobResult) Render added in v0.9.0

func (r GlobResult) Render() string

Render satisfies the Renderer contract. One match per line so the chat looks like running `find` or `fd` in a terminal.

type GrepMatch

type GrepMatch struct {
	Path   string `json:"path"`
	Line   int    `json:"line"`
	Column int    `json:"column"`
	Text   string `json:"text"`
}

GrepMatch is a single hit. Line and column are 1-indexed for human readability and to match conventional editor jumping.

type GrepResult

type GrepResult struct {
	BaseResult
	Matches      []GrepMatch `json:"matches"`
	MatchesCount int         `json:"matches_count"`
	Truncated    bool        `json:"truncated"`
	Cwd          string      `json:"cwd"`
	Pattern      string      `json:"pattern"`
}

GrepResult is the uniform shape returned regardless of engine.

func (GrepResult) Render added in v0.9.0

func (r GrepResult) Render() string

Render satisfies the Renderer contract. Output mirrors ripgrep's standard `path:line:col: text` so a developer reading the chat sees the same shape they'd see in a terminal.

type LineEndings

type LineEndings string

LineEndings identifies the dominant line-ending convention of a file.

const (
	LineEndingsLF      LineEndings = "lf"
	LineEndingsCRLF    LineEndings = "crlf"
	LineEndingsCR      LineEndings = "cr" // ancient Mac
	LineEndingsUnknown LineEndings = "unknown"
)

type ReadResult

type ReadResult struct {
	BaseResult
	Path       string `json:"path"`
	Content    string `json:"content"`
	LineStart  int    `json:"line_start"`
	LineEnd    int    `json:"line_end"`
	TotalLines int    `json:"total_lines"`
	SizeBytes  int64  `json:"size_bytes"`
	Format     string `json:"format"`
	Truncated  bool   `json:"truncated"`

	// Sheets is populated only for spreadsheet formats; lets the agent
	// page through workbook structure without re-reading the file.
	Sheets []string `json:"sheets,omitempty"`
}

ReadResult is the uniform shape across all formats. Embeds BaseResult so common fields (engine, duration, error) and their rendering helpers are inherited.

func (ReadResult) Render added in v0.9.0

func (r ReadResult) Render() string

Render satisfies the Renderer contract. The body is the file content framed by horizontal rules; header carries path and engine, footer carries cursor + size.

type Renderer added in v0.9.0

type Renderer interface {
	Render() string
}

Renderer is the contract every tool result implements. The MCP dispatch helper below relies on it exclusively; tools that don't implement it would fall through to JSON marshaling, but every core tool overrides it.

type ToolSearchResult

type ToolSearchResult struct {
	BaseResult
	Query        string       `json:"query"`
	Results      []search.Hit `json:"results"`
	TotalIndexed int          `json:"total_indexed"`
	TypeFilter   string       `json:"type_filter,omitempty"`
}

ToolSearchResult is the JSON envelope returned to the agent.

func (ToolSearchResult) Render added in v0.9.0

func (r ToolSearchResult) Render() string

Render satisfies the Renderer contract. Each hit is rendered as "[score] name (type) — description" so the agent and the user see a plausible top-N list rather than raw JSON.

type WebFetchResult

type WebFetchResult struct {
	BaseResult
	URL         string `json:"url"`
	FinalURL    string `json:"final_url"`
	Status      int    `json:"status"`
	ContentType string `json:"content_type"`
	Format      string `json:"format"` // "html" | "text" | "binary-rejected"
	Title       string `json:"title,omitempty"`
	Byline      string `json:"byline,omitempty"`
	SiteName    string `json:"site_name,omitempty"`
	Content     string `json:"content"`
	SizeBytes   int    `json:"size_bytes"`
	FetchedAt   string `json:"fetched_at"`
	Truncated   bool   `json:"truncated"`
}

WebFetchResult is the uniform shape returned to the agent.

func (WebFetchResult) Render added in v0.9.0

func (r WebFetchResult) Render() string

Render satisfies the Renderer contract. Header carries the URL + status + format; body is the extracted article (or raw text); footer notes truncation when applicable.

type WebSearchHit

type WebSearchHit struct {
	Title   string `json:"title"`
	URL     string `json:"url"`
	Snippet string `json:"snippet,omitempty"`
}

WebSearchHit is one ranked result from any backend.

type WebSearchResult

type WebSearchResult struct {
	BaseResult
	Query        string         `json:"query"`
	Results      []WebSearchHit `json:"results"`
	ResultsCount int            `json:"results_count"`
	Truncated    bool           `json:"truncated"`
}

WebSearchResult is the uniform result envelope the agent receives. Backend lives in BaseResult.Engine because the engine concept is the same — which backend ran this query — and consolidating in the embedded struct keeps every tool's "who did the work" field in one place across the catalog.

func (WebSearchResult) Render added in v0.9.0

func (r WebSearchResult) Render() string

Render satisfies the Renderer contract. Header carries query + backend; body lists `[N] title — url` rows so a developer scans results the same way they would in a browser results page.

type WriteResult

type WriteResult struct {
	BaseResult
	Path         string `json:"path"`
	BytesWritten int64  `json:"bytes_written"`
	Created      bool   `json:"created"`
	LineEndings  string `json:"line_endings"`
}

WriteResult is the uniform shape returned to the agent.

func (WriteResult) Render added in v0.9.0

func (r WriteResult) Render() string

Render satisfies the Renderer contract. The Operation field switches between "Write" and "Create" based on whether the file existed beforehand — agents glance-read whether something was clobbered.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL