mirror of
https://github.com/lukaszraczylo/harness-sync.git
synced 2026-10-12 00:45:22 +00:00
- common/mcp.go: MCPStyle enum + BuildMCPMapStyled for claude/crush/opencode/zed dialects - common/jsonmerge.go: MergeJSONKeys (JSON-first, JSONC fallback) preserves user keys; fix: try plain json.Unmarshal before StripJSONComments to avoid mangling URLs with // - common/yamlmerge.go: MergeYAMLKeys for YAML configs (goose) - common/blocks.go: add ProvidersAsMap for opencode map-shaped provider key - claudecode: merge settings.json (preserves hooks/permissions/env); MCPClaudeStyle (no type field) - crush: fix mcp key (was mcpServers); MCPCrushStyle entries include type field; merge crush.json; fix import key mcp - kilo: drop providers key (kilo has none); MCPOpencodeStyle (local/remote); merge kilo.json - opencode: fix provider (singular map) + model (not default_model); MCPOpencodeStyle; merge jsonc; fix import key mcp - goose: new adapter — YAML merge, extensions map, splitProviderModel, import reconstructs MCP from extensions - cagent: new adapter — renders starter default.yaml, import returns empty - zed: new adapter — merge settings.json, context_servers (MCPZedStyle) + agent.default_model - docs: append Grounded harness schemas section to design spec
119 lines
3.0 KiB
Go
119 lines
3.0 KiB
Go
// Package common provides shared helpers for harness adapters.
|
|
package common
|
|
|
|
import "github.com/lukaszraczylo/harness-sync/internal/canonical"
|
|
|
|
// ProviderEntry is a generic provider record. Adapters may massage further.
|
|
type ProviderEntry = map[string]any
|
|
|
|
// BuildProviders returns the canonical-to-generic provider list: gateway first
|
|
// (if configured), then any upstreams. Each entry has id, name, base_url,
|
|
// api_key, and (for the gateway) a models list.
|
|
func BuildProviders(p *canonical.Profile) []ProviderEntry {
|
|
out := make([]ProviderEntry, 0, len(p.Upstreams)+1)
|
|
if p.Gateway.URL != "" {
|
|
gw := ProviderEntry{
|
|
"id": "harness-sync-gateway",
|
|
"name": "harness-sync gateway",
|
|
"base_url": p.Gateway.URL,
|
|
"api_key": p.Gateway.Token,
|
|
}
|
|
if models := buildModels(p.Models); len(models) > 0 {
|
|
gw["models"] = models
|
|
}
|
|
out = append(out, gw)
|
|
}
|
|
for _, up := range p.Upstreams {
|
|
e := ProviderEntry{"id": up.Name, "name": up.Name}
|
|
if up.BaseURL != "" {
|
|
e["base_url"] = up.BaseURL
|
|
}
|
|
if up.APIKey != "" {
|
|
e["api_key"] = up.APIKey
|
|
}
|
|
out = append(out, e)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func buildModels(models []canonical.Model) []map[string]any {
|
|
out := make([]map[string]any, 0, len(models))
|
|
for _, m := range models {
|
|
e := map[string]any{"id": m.ID}
|
|
if m.Alias != "" {
|
|
e["alias"] = m.Alias
|
|
}
|
|
out = append(out, e)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ProvidersAsMap returns a name-keyed map of provider objects for harnesses
|
|
// like opencode that use `provider` (singular, map-shaped) instead of a slice.
|
|
// The gateway entry is keyed "harness-sync-gateway"; upstreams use their name.
|
|
func ProvidersAsMap(p *canonical.Profile) map[string]any {
|
|
out := map[string]any{}
|
|
if p.Gateway.URL != "" {
|
|
entry := map[string]any{
|
|
"name": "harness-sync gateway",
|
|
"options": map[string]any{
|
|
"baseURL": p.Gateway.URL,
|
|
"apiKey": p.Gateway.Token,
|
|
},
|
|
}
|
|
if models := buildModels(p.Models); len(models) > 0 {
|
|
modelsMap := map[string]any{}
|
|
for _, m := range p.Models {
|
|
modelsMap[m.ID] = map[string]any{"name": m.ID}
|
|
}
|
|
entry["models"] = modelsMap
|
|
}
|
|
out["harness-sync-gateway"] = entry
|
|
}
|
|
for _, up := range p.Upstreams {
|
|
entry := map[string]any{"name": up.Name}
|
|
opts := map[string]any{}
|
|
if up.BaseURL != "" {
|
|
opts["baseURL"] = up.BaseURL
|
|
}
|
|
if up.APIKey != "" {
|
|
opts["apiKey"] = up.APIKey
|
|
}
|
|
if len(opts) > 0 {
|
|
entry["options"] = opts
|
|
}
|
|
out[up.Name] = entry
|
|
}
|
|
return out
|
|
}
|
|
|
|
// BuildMCPMap returns canonical MCP servers as a name -> object map.
|
|
// Empty map (not nil) is returned when there are no servers so the caller
|
|
// can decide whether to include the key.
|
|
func BuildMCPMap(reg *canonical.MCPRegistry) map[string]any {
|
|
out := map[string]any{}
|
|
if reg == nil {
|
|
return out
|
|
}
|
|
for _, s := range reg.Servers {
|
|
e := map[string]any{}
|
|
if s.Command != "" {
|
|
e["command"] = s.Command
|
|
}
|
|
if len(s.Args) > 0 {
|
|
e["args"] = s.Args
|
|
}
|
|
if s.URL != "" {
|
|
e["url"] = s.URL
|
|
}
|
|
if s.Transport != "" {
|
|
e["transport"] = s.Transport
|
|
}
|
|
if len(s.Env) > 0 {
|
|
e["env"] = s.Env
|
|
}
|
|
out[s.Name] = e
|
|
}
|
|
return out
|
|
}
|