- Fix slashes in agent identity causing invalid hook file paths (gt-vqhc) - Add Prerequisites section to README (gt-vzic) - Create CONTRIBUTING.md, CODE_OF_CONDUCT.md, SECURITY.md (gt-xbfw) - Update Install section for future package managers (gt-7wcf) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
130 lines
3.1 KiB
Go
130 lines
3.1 KiB
Go
package wisp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// Common errors.
|
|
var (
|
|
ErrNoWispDir = errors.New("beads directory does not exist")
|
|
ErrNoHook = errors.New("no hook file found")
|
|
ErrInvalidWisp = errors.New("invalid hook file format")
|
|
)
|
|
|
|
// EnsureDir ensures the .beads directory exists in the given root.
|
|
func EnsureDir(root string) (string, error) {
|
|
dir := filepath.Join(root, WispDir)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return "", fmt.Errorf("create beads dir: %w", err)
|
|
}
|
|
return dir, nil
|
|
}
|
|
|
|
// WispPath returns the full path to a file in the beads directory.
|
|
func WispPath(root, filename string) string {
|
|
return filepath.Join(root, WispDir, filename)
|
|
}
|
|
|
|
// HookPath returns the full path to an agent's hook file.
|
|
func HookPath(root, agent string) string {
|
|
return WispPath(root, HookFilename(agent))
|
|
}
|
|
|
|
// WriteSlungWork writes a slung work hook to the agent's hook file.
|
|
func WriteSlungWork(root, agent string, sw *SlungWork) error {
|
|
dir, err := EnsureDir(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
path := filepath.Join(dir, HookFilename(agent))
|
|
return writeJSON(path, sw)
|
|
}
|
|
|
|
// ReadHook reads the slung work from an agent's hook file.
|
|
// Returns ErrNoHook if no hook file exists.
|
|
func ReadHook(root, agent string) (*SlungWork, error) {
|
|
path := HookPath(root, agent)
|
|
|
|
data, err := os.ReadFile(path)
|
|
if os.IsNotExist(err) {
|
|
return nil, ErrNoHook
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read hook: %w", err)
|
|
}
|
|
|
|
var sw SlungWork
|
|
if err := json.Unmarshal(data, &sw); err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrInvalidWisp, err)
|
|
}
|
|
|
|
if sw.Type != TypeSlungWork {
|
|
return nil, fmt.Errorf("%w: expected slung-work, got %s", ErrInvalidWisp, sw.Type)
|
|
}
|
|
|
|
return &sw, nil
|
|
}
|
|
|
|
// BurnHook removes an agent's hook file after it has been picked up.
|
|
func BurnHook(root, agent string) error {
|
|
path := HookPath(root, agent)
|
|
err := os.Remove(path)
|
|
if os.IsNotExist(err) {
|
|
return nil // already burned
|
|
}
|
|
return err
|
|
}
|
|
|
|
// HasHook checks if an agent has a hook file.
|
|
func HasHook(root, agent string) bool {
|
|
path := HookPath(root, agent)
|
|
_, err := os.Stat(path)
|
|
return err == nil
|
|
}
|
|
|
|
// ListHooks returns a list of agents with active hooks.
|
|
func ListHooks(root string) ([]string, error) {
|
|
dir := filepath.Join(root, WispDir)
|
|
entries, err := os.ReadDir(dir)
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var agents []string
|
|
for _, e := range entries {
|
|
if agent := AgentFromHookFilename(e.Name()); agent != "" {
|
|
agents = append(agents, agent)
|
|
}
|
|
}
|
|
return agents, nil
|
|
}
|
|
|
|
// writeJSON is a helper to write JSON files atomically.
|
|
func writeJSON(path string, v interface{}) error {
|
|
data, err := json.MarshalIndent(v, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("marshal json: %w", err)
|
|
}
|
|
|
|
// Write to temp file then rename for atomicity
|
|
tmp := path + ".tmp"
|
|
if err := os.WriteFile(tmp, data, 0644); err != nil {
|
|
return fmt.Errorf("write temp: %w", err)
|
|
}
|
|
|
|
if err := os.Rename(tmp, path); err != nil {
|
|
os.Remove(tmp) // cleanup on failure
|
|
return fmt.Errorf("rename: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|