feat(bugseti): add marketplace MCP root
- add MarketplaceMCPRoot config and UI setting\n- prefer config root before env or auto-discovery\n- thread config root into ethics guard usage
This commit is contained in:
parent
23d9c4da19
commit
00c011bd39
8 changed files with 65 additions and 19 deletions
|
|
@ -9,6 +9,7 @@ interface Config {
|
||||||
notificationsEnabled: boolean;
|
notificationsEnabled: boolean;
|
||||||
notificationSound: boolean;
|
notificationSound: boolean;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
|
marketplaceMcpRoot: string;
|
||||||
theme: string;
|
theme: string;
|
||||||
autoSeedContext: boolean;
|
autoSeedContext: boolean;
|
||||||
workHours?: {
|
workHours?: {
|
||||||
|
|
@ -161,6 +162,13 @@ interface Config {
|
||||||
<input type="text" class="form-input" [(ngModel)]="config.workspaceDir"
|
<input type="text" class="form-input" [(ngModel)]="config.workspaceDir"
|
||||||
placeholder="Leave empty for default">
|
placeholder="Leave empty for default">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Marketplace MCP Root</label>
|
||||||
|
<input type="text" class="form-input" [(ngModel)]="config.marketplaceMcpRoot"
|
||||||
|
placeholder="Path to core-agent (optional)">
|
||||||
|
<p class="section-description">Override the marketplace MCP root. Leave empty to auto-detect.</p>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -308,6 +316,7 @@ export class SettingsComponent implements OnInit {
|
||||||
notificationsEnabled: true,
|
notificationsEnabled: true,
|
||||||
notificationSound: true,
|
notificationSound: true,
|
||||||
workspaceDir: '',
|
workspaceDir: '',
|
||||||
|
marketplaceMcpRoot: '',
|
||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
autoSeedContext: true,
|
autoSeedContext: true,
|
||||||
workHours: {
|
workHours: {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize core services
|
// Initialize core services
|
||||||
notifyService := bugseti.NewNotifyService()
|
notifyService := bugseti.NewNotifyService(configService)
|
||||||
statsService := bugseti.NewStatsService(configService)
|
statsService := bugseti.NewStatsService(configService)
|
||||||
fetcherService := bugseti.NewFetcherService(configService, notifyService)
|
fetcherService := bugseti.NewFetcherService(configService, notifyService)
|
||||||
queueService := bugseti.NewQueueService(configService)
|
queueService := bugseti.NewQueueService(configService)
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ type Config struct {
|
||||||
// Workspace
|
// Workspace
|
||||||
WorkspaceDir string `json:"workspaceDir,omitempty"`
|
WorkspaceDir string `json:"workspaceDir,omitempty"`
|
||||||
DataDir string `json:"dataDir,omitempty"`
|
DataDir string `json:"dataDir,omitempty"`
|
||||||
|
// Marketplace MCP
|
||||||
|
MarketplaceMCPRoot string `json:"marketplaceMcpRoot,omitempty"`
|
||||||
|
|
||||||
// Onboarding
|
// Onboarding
|
||||||
Onboarded bool `json:"onboarded"`
|
Onboarded bool `json:"onboarded"`
|
||||||
|
|
@ -96,6 +98,7 @@ func NewConfigService() *ConfigService {
|
||||||
MaxConcurrentIssues: 1,
|
MaxConcurrentIssues: 1,
|
||||||
AutoSeedContext: true,
|
AutoSeedContext: true,
|
||||||
DataDir: bugsetiDir,
|
DataDir: bugsetiDir,
|
||||||
|
MarketplaceMCPRoot: "",
|
||||||
UpdateChannel: "stable",
|
UpdateChannel: "stable",
|
||||||
AutoUpdate: false,
|
AutoUpdate: false,
|
||||||
UpdateCheckInterval: 6, // Check every 6 hours
|
UpdateCheckInterval: 6, // Check every 6 hours
|
||||||
|
|
@ -181,6 +184,13 @@ func (c *ConfigService) GetConfig() Config {
|
||||||
return *c.config
|
return *c.config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMarketplaceMCPRoot returns the configured marketplace MCP root path.
|
||||||
|
func (c *ConfigService) GetMarketplaceMCPRoot() string {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.config.MarketplaceMCPRoot
|
||||||
|
}
|
||||||
|
|
||||||
// SetConfig updates the configuration and saves it.
|
// SetConfig updates the configuration and saves it.
|
||||||
func (c *ConfigService) SetConfig(config Config) error {
|
func (c *ConfigService) SetConfig(config Config) error {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
|
|
|
||||||
|
|
@ -26,19 +26,32 @@ type EthicsGuard struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ethicsGuardOnce sync.Once
|
ethicsGuardMu sync.Mutex
|
||||||
ethicsGuard *EthicsGuard
|
ethicsGuard *EthicsGuard
|
||||||
|
ethicsGuardRoot string
|
||||||
)
|
)
|
||||||
|
|
||||||
func getEthicsGuard(ctx context.Context) *EthicsGuard {
|
func getEthicsGuard(ctx context.Context) *EthicsGuard {
|
||||||
ethicsGuardOnce.Do(func() {
|
return getEthicsGuardWithRoot(ctx, "")
|
||||||
guard := loadEthicsGuard(ctx)
|
}
|
||||||
if guard == nil {
|
|
||||||
guard = &EthicsGuard{}
|
|
||||||
}
|
|
||||||
ethicsGuard = guard
|
|
||||||
})
|
|
||||||
|
|
||||||
|
func getEthicsGuardWithRoot(ctx context.Context, rootHint string) *EthicsGuard {
|
||||||
|
rootHint = strings.TrimSpace(rootHint)
|
||||||
|
|
||||||
|
ethicsGuardMu.Lock()
|
||||||
|
defer ethicsGuardMu.Unlock()
|
||||||
|
|
||||||
|
if ethicsGuard != nil && ethicsGuardRoot == rootHint {
|
||||||
|
return ethicsGuard
|
||||||
|
}
|
||||||
|
|
||||||
|
guard := loadEthicsGuard(ctx, rootHint)
|
||||||
|
if guard == nil {
|
||||||
|
guard = &EthicsGuard{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ethicsGuard = guard
|
||||||
|
ethicsGuardRoot = rootHint
|
||||||
if ethicsGuard == nil {
|
if ethicsGuard == nil {
|
||||||
return &EthicsGuard{}
|
return &EthicsGuard{}
|
||||||
}
|
}
|
||||||
|
|
@ -67,14 +80,14 @@ func guardFromMarketplace(ctx context.Context, client marketplaceClient) *Ethics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadEthicsGuard(ctx context.Context) *EthicsGuard {
|
func loadEthicsGuard(ctx context.Context, rootHint string) *EthicsGuard {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
client, err := newMarketplaceClient(ctx)
|
client, err := newMarketplaceClient(ctx, rootHint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &EthicsGuard{}
|
return &EthicsGuard{}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,12 +60,12 @@ type mcpMarketplaceClient struct {
|
||||||
client *client.Client
|
client *client.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMarketplaceClient(ctx context.Context) (marketplaceClient, error) {
|
func newMarketplaceClient(ctx context.Context, rootHint string) (marketplaceClient, error) {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
|
||||||
command, args, err := resolveMarketplaceCommand()
|
command, args, err := resolveMarketplaceCommand(rootHint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -180,12 +180,17 @@ func toolResultMessage(result *mcp.CallToolResult) string {
|
||||||
return "unknown error"
|
return "unknown error"
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveMarketplaceCommand() (string, []string, error) {
|
func resolveMarketplaceCommand(rootHint string) (string, []string, error) {
|
||||||
if command := strings.TrimSpace(os.Getenv("BUGSETI_MCP_COMMAND")); command != "" {
|
if command := strings.TrimSpace(os.Getenv("BUGSETI_MCP_COMMAND")); command != "" {
|
||||||
args := strings.Fields(os.Getenv("BUGSETI_MCP_ARGS"))
|
args := strings.Fields(os.Getenv("BUGSETI_MCP_ARGS"))
|
||||||
return command, args, nil
|
return command, args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if root := strings.TrimSpace(rootHint); root != "" {
|
||||||
|
path := filepath.Join(root, "mcp")
|
||||||
|
return "go", []string{"run", path}, nil
|
||||||
|
}
|
||||||
|
|
||||||
if root := strings.TrimSpace(os.Getenv("BUGSETI_MCP_ROOT")); root != "" {
|
if root := strings.TrimSpace(os.Getenv("BUGSETI_MCP_ROOT")); root != "" {
|
||||||
path := filepath.Join(root, "mcp")
|
path := filepath.Join(root, "mcp")
|
||||||
return "go", []string{"run", path}, nil
|
return "go", []string{"run", path}, nil
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,15 @@ import (
|
||||||
type NotifyService struct {
|
type NotifyService struct {
|
||||||
enabled bool
|
enabled bool
|
||||||
sound bool
|
sound bool
|
||||||
|
config *ConfigService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewNotifyService creates a new NotifyService.
|
// NewNotifyService creates a new NotifyService.
|
||||||
func NewNotifyService() *NotifyService {
|
func NewNotifyService(config *ConfigService) *NotifyService {
|
||||||
return &NotifyService{
|
return &NotifyService{
|
||||||
enabled: true,
|
enabled: true,
|
||||||
sound: true,
|
sound: true,
|
||||||
|
config: config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,7 +47,7 @@ func (n *NotifyService) Notify(title, message string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
guard := getEthicsGuard(context.Background())
|
guard := getEthicsGuardWithRoot(context.Background(), n.getMarketplaceRoot())
|
||||||
safeTitle := guard.SanitizeNotification(title)
|
safeTitle := guard.SanitizeNotification(title)
|
||||||
safeMessage := guard.SanitizeNotification(message)
|
safeMessage := guard.SanitizeNotification(message)
|
||||||
|
|
||||||
|
|
@ -72,6 +74,13 @@ func (n *NotifyService) Notify(title, message string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NotifyService) getMarketplaceRoot() string {
|
||||||
|
if n == nil || n.config == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return n.config.GetMarketplaceMCPRoot()
|
||||||
|
}
|
||||||
|
|
||||||
// NotifyIssue sends a notification about a new issue.
|
// NotifyIssue sends a notification about a new issue.
|
||||||
func (n *NotifyService) NotifyIssue(issue *Issue) error {
|
func (n *NotifyService) NotifyIssue(issue *Issue) error {
|
||||||
title := "New Issue Available"
|
title := "New Issue Available"
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ func (s *SeederService) SeedIssue(issue *Issue) (*IssueContext, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Seed skill failed, using fallback: %v", err)
|
log.Printf("Seed skill failed, using fallback: %v", err)
|
||||||
// Fallback to basic context preparation
|
// Fallback to basic context preparation
|
||||||
guard := getEthicsGuard(context.Background())
|
guard := getEthicsGuardWithRoot(context.Background(), s.config.GetMarketplaceMCPRoot())
|
||||||
ctx = s.prepareBasicContext(issue, guard)
|
ctx = s.prepareBasicContext(issue, guard)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,7 +95,7 @@ func (s *SeederService) runSeedSkill(issue *Issue, workDir string) (*IssueContex
|
||||||
mcpCtx, mcpCancel := context.WithTimeout(ctx, 20*time.Second)
|
mcpCtx, mcpCancel := context.WithTimeout(ctx, 20*time.Second)
|
||||||
defer mcpCancel()
|
defer mcpCancel()
|
||||||
|
|
||||||
marketplace, err := newMarketplaceClient(mcpCtx)
|
marketplace, err := newMarketplaceClient(mcpCtx, s.config.GetMarketplaceMCPRoot())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ func (s *SubmitService) Submit(submission *PRSubmission) (*PRResult, error) {
|
||||||
return nil, fmt.Errorf("work directory not specified")
|
return nil, fmt.Errorf("work directory not specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
guard := getEthicsGuard(context.Background())
|
guard := getEthicsGuardWithRoot(context.Background(), s.config.GetMarketplaceMCPRoot())
|
||||||
issueTitle := guard.SanitizeTitle(issue.Title)
|
issueTitle := guard.SanitizeTitle(issue.Title)
|
||||||
|
|
||||||
// Step 1: Ensure we have a fork
|
// Step 1: Ensure we have a fork
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue