feat(framework): add service layer to git and agentic packages

- Add pkg/framework/framework.go for cleaner imports
- Add pkg/git/service.go with Core service wrapper
- Add pkg/agentic/service.go with AI/Claude service wrapper
- Services use IPC pattern with ACTION() dispatch

Usage:
  import "github.com/host-uk/core/pkg/framework"

  app, _ := framework.New(
      framework.WithService(git.NewService(git.ServiceOptions{})),
      framework.WithServiceLock(),
  )

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-30 09:19:20 +00:00
parent a0f4baafad
commit c21a271dcf
3 changed files with 276 additions and 0 deletions

107
pkg/agentic/service.go Normal file
View file

@ -0,0 +1,107 @@
package agentic
import (
"context"
"os"
"os/exec"
"github.com/host-uk/core/pkg/framework"
)
// Actions for AI service IPC
// ActionCommit requests Claude to create a commit.
type ActionCommit struct {
Path string
Name string
CanEdit bool // allow Write/Edit tools
}
// ActionPrompt sends a custom prompt to Claude.
type ActionPrompt struct {
Prompt string
WorkDir string
AllowedTools []string
}
// ServiceOptions for configuring the AI service.
type ServiceOptions struct {
DefaultTools []string
}
// DefaultServiceOptions returns sensible defaults.
func DefaultServiceOptions() ServiceOptions {
return ServiceOptions{
DefaultTools: []string{"Bash", "Read", "Glob", "Grep"},
}
}
// Service provides AI/Claude operations as a Core service.
type Service struct {
*framework.ServiceRuntime[ServiceOptions]
}
// NewService creates an AI service factory.
func NewService(opts ServiceOptions) func(*framework.Core) (any, error) {
return func(c *framework.Core) (any, error) {
return &Service{
ServiceRuntime: framework.NewServiceRuntime(c, opts),
}, nil
}
}
// OnStartup registers action handlers.
func (s *Service) OnStartup(ctx context.Context) error {
s.Core().RegisterAction(s.handle)
return nil
}
func (s *Service) handle(c *framework.Core, msg framework.Message) error {
switch m := msg.(type) {
case ActionCommit:
return s.handleCommit(m)
case ActionPrompt:
return s.handlePrompt(m)
}
return nil
}
func (s *Service) handleCommit(action ActionCommit) error {
prompt := Prompt("commit")
tools := "Bash,Read,Glob,Grep"
if action.CanEdit {
tools = "Bash,Read,Write,Edit,Glob,Grep"
}
cmd := exec.CommandContext(context.Background(), "claude", "-p", prompt, "--allowedTools", tools)
cmd.Dir = action.Path
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
return cmd.Run()
}
func (s *Service) handlePrompt(action ActionPrompt) error {
tools := "Bash,Read,Glob,Grep"
if len(action.AllowedTools) > 0 {
tools = ""
for i, t := range action.AllowedTools {
if i > 0 {
tools += ","
}
tools += t
}
}
cmd := exec.CommandContext(context.Background(), "claude", "-p", action.Prompt, "--allowedTools", tools)
if action.WorkDir != "" {
cmd.Dir = action.WorkDir
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
return cmd.Run()
}

View file

@ -0,0 +1,67 @@
// Package framework provides the Core DI/service framework.
// Import this package for cleaner access to the framework types.
//
// Usage:
//
// import "github.com/host-uk/core/pkg/framework"
//
// app, _ := framework.New(
// framework.WithServiceLock(),
// )
package framework
import (
core "github.com/host-uk/core/pkg/framework/core"
)
// Re-export core types for cleaner imports
type (
Core = core.Core
Option = core.Option
Message = core.Message
Startable = core.Startable
Stoppable = core.Stoppable
Config = core.Config
Display = core.Display
WindowOption = core.WindowOption
Features = core.Features
Contract = core.Contract
Error = core.Error
ServiceRuntime[T any] = core.ServiceRuntime[T]
Runtime = core.Runtime
ServiceFactory = core.ServiceFactory
)
// Re-export core functions
var (
New = core.New
WithService = core.WithService
WithName = core.WithName
WithApp = core.WithApp
WithAssets = core.WithAssets
WithServiceLock = core.WithServiceLock
App = core.App
E = core.E
NewRuntime = core.NewRuntime
NewWithFactories = core.NewWithFactories
)
// NewServiceRuntime creates a new ServiceRuntime for a service.
func NewServiceRuntime[T any](c *Core, opts T) *ServiceRuntime[T] {
return core.NewServiceRuntime(c, opts)
}
// Re-export generic functions
func ServiceFor[T any](c *Core, name string) (T, error) {
return core.ServiceFor[T](c, name)
}
func MustServiceFor[T any](c *Core, name string) T {
return core.MustServiceFor[T](c, name)
}
// Action types
type (
ActionServiceStartup = core.ActionServiceStartup
ActionServiceShutdown = core.ActionServiceShutdown
)

102
pkg/git/service.go Normal file
View file

@ -0,0 +1,102 @@
package git
import (
"context"
"github.com/host-uk/core/pkg/framework"
)
// Actions for git service IPC
// ActionStatus requests git status for paths.
type ActionStatus struct {
Paths []string
Names map[string]string
}
// ActionPush requests git push for a path.
type ActionPush struct{ Path, Name string }
// ActionPull requests git pull for a path.
type ActionPull struct{ Path, Name string }
// ServiceOptions for configuring the git service.
type ServiceOptions struct {
WorkDir string
}
// Service provides git operations as a Core service.
type Service struct {
*framework.ServiceRuntime[ServiceOptions]
lastStatus []RepoStatus
}
// NewService creates a git service factory.
func NewService(opts ServiceOptions) func(*framework.Core) (any, error) {
return func(c *framework.Core) (any, error) {
return &Service{
ServiceRuntime: framework.NewServiceRuntime(c, opts),
}, nil
}
}
// OnStartup registers action handlers.
func (s *Service) OnStartup(ctx context.Context) error {
s.Core().RegisterAction(s.handle)
return nil
}
func (s *Service) handle(c *framework.Core, msg framework.Message) error {
switch m := msg.(type) {
case ActionStatus:
return s.handleStatus(m)
case ActionPush:
return s.handlePush(m)
case ActionPull:
return s.handlePull(m)
}
return nil
}
func (s *Service) handleStatus(action ActionStatus) error {
ctx := context.Background()
statuses := Status(ctx, StatusOptions{
Paths: action.Paths,
Names: action.Names,
})
s.lastStatus = statuses
return nil
}
func (s *Service) handlePush(action ActionPush) error {
return Push(context.Background(), action.Path)
}
func (s *Service) handlePull(action ActionPull) error {
return Pull(context.Background(), action.Path)
}
// Status returns last status result.
func (s *Service) Status() []RepoStatus { return s.lastStatus }
// DirtyRepos returns repos with uncommitted changes.
func (s *Service) DirtyRepos() []RepoStatus {
var dirty []RepoStatus
for _, st := range s.lastStatus {
if st.Error == nil && st.IsDirty() {
dirty = append(dirty, st)
}
}
return dirty
}
// AheadRepos returns repos with unpushed commits.
func (s *Service) AheadRepos() []RepoStatus {
var ahead []RepoStatus
for _, st := range s.lastStatus {
if st.Error == nil && st.HasUnpushed() {
ahead = append(ahead, st)
}
}
return ahead
}