Documentation
¶
Overview ¶
Package usercmd loads user-authored slash commands from markdown files dropped in ~/.yottacode/commands/ (user scope) and <cwd>/.yottacode/commands/ (project scope).
Layout mirrors Claude Code's custom-commands feature: filename becomes the command name, subdirectories become a colon-separated namespace (commands/frontend/component.md → /frontend:component), the body is sent to the agent as the user message for that turn (with $ARGUMENTS / $1..$9 substituted and @<path> file refs honored by the same pipeline that handles user-typed @-refs).
Index ¶
- Variables
- func DisplayPath(cmd Command) string
- func Load(cwd string) (cmds []Command, errs []LoadError)
- func LoadAll(cwd string) (cmds []Command, errs []LoadError)
- func LoadDefaults() (cmds []Command, errs []LoadError)
- func ProjectCommandsDir(cwd string) string
- func Substitute(body, args string) string
- func UserCommandsDir() (string, error)
- type Command
- type Level
- type LoadError
- type Scope
Constants ¶
This section is empty.
Variables ¶
var Reserved = map[string]bool{ "help": true, "quit": true, "clear": true, "permissions": true, "system": true, "sessions": true, "model": true, "provider": true, "doctor": true, "redo": true, "recall": true, "summarize": true, "checkpoints": true, "memory": true, "max-iterations": true, "setup": true, "init": true, "plan": true, "subagents": true, }
Reserved is the set of built-in slash command names a custom file is forbidden from shadowing. Kept here (not derived from the TUI package's allSlash) so this package stays standalone-testable; the TUI's init() is the source of truth and a one-line drift test in the TUI package guards against new built-ins being forgotten.
Functions ¶
func DisplayPath ¶
DisplayPath returns the path string to show in /help for a command. For on-disk commands it's the SourceFile as-is; for defaults it renders a "(default)" tag instead of the FS-internal path which would be useless to the user (they can't `vim` the embed.FS).
func Load ¶
Load walks both scope directories (user first, then project) and returns the merged command list plus per-file load errors.
Conflict resolution:
- Same name within the SAME scope: both copies dropped, error emitted. We can't pick a winner safely (the user clearly intended different prompts) and silently keeping one would surprise them.
- Same name across scopes: project wins. The user-scope copy is dropped with an info-level notice so the user understands why /foo doesn't behave like their global file.
- Same name as a built-in: custom dropped with a warning. Built-ins can't be shadowed because the dispatcher walks built-ins first.
Errors are returned alongside the commands rather than as a single error — one broken file should not block a directory of working commands. The TUI iterates errs at startup and emits one styled notice per entry.
func LoadAll ¶
LoadAll merges the embedded defaults with the on-disk user/project commands returned by Load. Precedence, highest first:
- Project scope (`<cwd>/.yottacode/commands/`)
- User scope (`~/.yottacode/commands/`)
- Default scope (embedded into the binary)
When a user or project command shadows a default of the same name, the default is silently replaced — that's the *expected* behavior of "ship a starter kit the user can customize," not a misconfiguration worth a startup notice. The same-scope and built-in shadow warnings from Load() still flow through unchanged.
func LoadDefaults ¶
LoadDefaults parses the embedded built-in commands. The shape mirrors `loadScope` (file → Command) but reads from the embed.FS instead of the disk and never returns "duplicate name" errors (the repo's own file layout enforces uniqueness — duplicates can't exist at build time).
All returned commands carry Scope=ScopeDefault. SourceFile is set to the FS-relative path (e.g. `defaults/git/commit-message.md`) so /help can render a "(default)" tag instead of a real on-disk path.
func ProjectCommandsDir ¶
ProjectCommandsDir returns <cwd>/.yottacode/commands/ — the per-repo custom-command directory. Commands here are committable so teams can share project-specific commands via git. Empty cwd returns "" so callers can detect "no project scope" without an error path.
func Substitute ¶
Substitute resolves $ARGUMENTS and $1..$9 in body using args. args is the post-name remainder a user typed after the command — for "/foo a b c", args is "a b c".
Tokenization is strings.Fields (whitespace-split, collapsing runs) to match what runSlash already does on the command line. Missing positionals (e.g. $3 when only two args were supplied) resolve to the empty string; the command author is responsible for handling the empty case in their prose if it matters.
func UserCommandsDir ¶
UserCommandsDir returns ~/.yottacode/commands/ — the cross-project custom-command directory. Commands here apply to every yottacode session for this user.
Types ¶
type Command ¶
type Command struct {
Name string
Description string
ArgHint string
Body string
SourceFile string
Scope Scope
}
Command is one loaded custom command. Body is the post-frontmatter markdown, unsubstituted (the TUI calls Substitute() with the user's args at invocation time).
type Level ¶
type Level int
Level grades a LoadError as either a hard error (the file did not produce a usable command) or a warning (the file is fine but shadowed / dropped due to a collision). The TUI renders the two levels with different styles.
type LoadError ¶
LoadError describes one problem encountered during Load. The file path is always populated so the user can locate the offending file.
type Scope ¶
type Scope string
Scope tags where a command came from. Surfaces in /help so users can see whether a command they're looking at lives on disk (user or project) or is shipped with the binary (default).
const ( // ScopeDefault is for commands embedded into the binary as the // built-in starter kit (`/git:commit-message`, `/check:review`, // etc.). Lowest precedence — a user or project file with the // same name shadows the default. The body is editable by copying // the default to ~/.yottacode/commands/<same-path>.md. ScopeDefault Scope = "default" ScopeUser Scope = "user" ScopeProject Scope = "project" )