mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-13 19:20:06 +00:00
7f366eab84
* cmd: add --no-skills, --skill, and --skills-dir CLI flags
The pkg/kit Options struct already had full backend support for skills
control (NoSkills, Skills []string, SkillsDir) wired into loadSkills()
in pkg/kit/kit.go, but there were no corresponding CLI flags to drive
them. This commit closes that gap.
Changes in cmd/root.go:
- Add three package-level flag variables alongside the existing
noExtensionsFlag/extensionPaths group:
noSkillsFlag bool
skillsPaths []string
skillsDir string
- Register three persistent cobra flags in init():
--no-skills disable skill loading (auto-discovery and explicit)
--skill <path> load a skill file or directory (repeatable)
--skills-dir <dir> override the project-local skills directory
used for auto-discovery
- Wire all three into the kitOpts struct literal in runNormalMode()
so they flow directly into kit.New() -> loadSkills().
No changes to pkg/kit or internal/skills -- the backend was already
complete. No viper binding is needed because kit.go reads these fields
directly from opts rather than from viper (unlike NoExtensions which
uses the viper fallback path).
Example usage:
kit --no-skills "prompt"
kit --skill ./my-skill.md --skill ./other-skill.md "prompt"
kit --skills-dir /path/to/skills "prompt"
Co-authored-by: Claude <claude@anthropic.com>
* docs: document --no-skills, --skill, and --skills-dir CLI flags
Add the three new skills CLI flags to all relevant documentation:
- README.md: add Skills section under Global Flags CLI reference
- www/pages/cli/flags.md: add Skills table (mirrors Extensions section pattern)
- www/pages/cli/commands.md: expand the Skills section with usage examples
and a description of auto-discovery vs explicit loading vs --no-skills
Co-authored-by: Claude <claude@anthropic.com>
* feat: add config file support for skills options
Skills could previously only be controlled via CLI flags or SDK Options
fields. This commit wires all three skills settings into viper so they
can also be set in .kit.yml / .kit.yaml / .kit.json and via KIT_*
environment variables — matching the pattern used by no-extensions,
no-core-tools, and prompt-template.
cmd/root.go:
- Bind --no-skills, --skill, and --skills-dir flags to viper keys
(no-skills, skill, skills-dir) so config file values flow through.
pkg/kit/kit.go:
- At skill-load time, merge opts fields with viper values:
- noSkills = opts.NoSkills || v.GetBool("no-skills")
- skillPaths: opts.Skills if non-empty, else v.GetStringSlice("skill")
- skillsDir: opts.SkillsDir if non-empty, else v.GetString("skills-dir")
- Build a shallow-copied mergedOpts so loadSkills() picks up the
resolved values without mutating the original Options struct.
docs:
- README.md: add skills keys to the Basic Configuration YAML example
- www/pages/configuration.md: add no-skills, skill, skills-dir rows to
the All configuration keys table
Config file example (.kit.yml):
no-skills: false
skill:
- /path/to/skill.md
skills-dir: /path/to/skills/
Co-authored-by: Claude <claude@anthropic.com>
* config: add skills keys to default .kit.yml template
Add no-skills, skill, and skills-dir as commented-out examples in the
default config file generated by EnsureConfigExists(), alongside the
existing application settings block.
Co-authored-by: Claude <claude@anthropic.com>
* test: add test coverage for skills CLI flags and config keys
Four test locations updated:
pkg/kit/export_test.go:
- Add ConfigStringSliceForTest() helper to expose v.GetStringSlice()
from the Kit's isolated viper store, needed to assert skill list values.
pkg/kit/kit_test.go (TestNewWithSkillsOptions):
- NoSkills=true: GetSkills() returns empty slice
- SkillsDir=<empty dir>: kit.New() succeeds with zero skills
- Skills=[file]: single explicit skill file is loaded and name parsed correctly
pkg/kit/viper_isolation_test.go:
- TestSkillsViperKeys: no-API-key struct-level checks for NoSkills, Skills,
and SkillsDir fields on Options
- TestSkillsConfigFileKeys: full kit.New() round-trips via a written .kit.yml
for each of the three config keys:
no-skills: true → GetSkills() returns empty
skill: [path] → named skill loaded from config file path
skills-dir: dir → custom discovery root accepted without error
internal/config/config_test.go (TestEnsureConfigExists):
- Assert generated ~/.kit.yml template contains '# Skills configuration',
'no-skills:', and 'skills-dir:' comment blocks.
Co-authored-by: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
591 lines
25 KiB
Go
591 lines
25 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/spf13/viper"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// MCPServerConfig represents configuration for an MCP server, supporting both
|
|
// local (stdio) and remote (StreamableHTTP/SSE) server types.
|
|
// It maintains backward compatibility with legacy configuration formats.
|
|
type MCPServerConfig struct {
|
|
Type string `json:"type"`
|
|
Command []string `json:"command,omitempty"`
|
|
Environment map[string]string `json:"environment,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
AllowedTools []string `json:"allowedTools,omitempty" yaml:"allowedTools,omitempty"`
|
|
ExcludedTools []string `json:"excludedTools,omitempty" yaml:"excludedTools,omitempty"`
|
|
|
|
// OAuth configuration for remote servers that don't support dynamic
|
|
// client registration (e.g. GitHub). When OAuthClientID is set, it is
|
|
// passed directly to the transport's OAuthConfig instead of relying on
|
|
// dynamic registration.
|
|
OAuthClientID string `json:"oauthClientId,omitempty" yaml:"oauthClientId,omitempty"`
|
|
OAuthClientSecret string `json:"oauthClientSecret,omitempty" yaml:"oauthClientSecret,omitempty"`
|
|
OAuthScopes []string `json:"oauthScopes,omitempty" yaml:"oauthScopes,omitempty"`
|
|
|
|
// NoOAuth disables OAuth transport configuration for this server, even
|
|
// when the connection pool has an auth handler. Use this for public MCP
|
|
// servers (e.g. PubMed) that don't require authentication. Without this
|
|
// flag, the pool would attach OAuth transport to every remote server,
|
|
// causing proactive dynamic-client-registration attempts that fail on
|
|
// servers that don't support it.
|
|
NoOAuth bool `json:"noOAuth,omitempty" yaml:"noOAuth,omitempty"`
|
|
|
|
// TasksMode controls when this server's tools/call requests are augmented
|
|
// with MCP task metadata (turning a synchronous call into an asynchronous,
|
|
// pollable job — see https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks).
|
|
//
|
|
// Valid values:
|
|
// - "" or "auto": (default) augment requests with task metadata only
|
|
// when the server advertises tasks/toolCalls capability during initialize.
|
|
// - "never": never augment — every tool call is synchronous, regardless
|
|
// of server capability.
|
|
// - "always": always augment, even when the server didn't advertise
|
|
// task support. The server may still respond synchronously; this just
|
|
// opts in unconditionally on the client side.
|
|
//
|
|
// In all modes, when the server returns a CreateTaskResult the client polls
|
|
// tasks/get / tasks/result until the task reaches a terminal state.
|
|
TasksMode string `json:"tasksMode,omitempty" yaml:"tasksMode,omitempty"`
|
|
|
|
// InProcessServer holds a live *server.MCPServer for in-process transport.
|
|
// When set (and Type is "inprocess"), the connection pool creates an
|
|
// in-process client instead of spawning a subprocess or making HTTP calls.
|
|
// This field is never serialized — it is only used programmatically via the SDK.
|
|
InProcessServer any `json:"-" yaml:"-"`
|
|
|
|
// Legacy fields for backward compatibility
|
|
Transport string `json:"transport,omitempty"`
|
|
Args []string `json:"args,omitempty"`
|
|
Env map[string]any `json:"env,omitempty"`
|
|
Headers []string `json:"headers,omitempty"`
|
|
}
|
|
|
|
// UnmarshalJSON handles both new and legacy config formats for backward compatibility.
|
|
// New format uses "type" field with "local", "remote", or "builtin" values.
|
|
// Legacy format uses "transport", "command", "args", and "env" fields.
|
|
func (s *MCPServerConfig) UnmarshalJSON(data []byte) error {
|
|
// First try to unmarshal as the new format
|
|
type newFormat struct {
|
|
Type string `json:"type"`
|
|
Command []string `json:"command,omitempty"`
|
|
Environment map[string]string `json:"environment,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
Headers []string `json:"headers,omitempty"`
|
|
AllowedTools []string `json:"allowedTools,omitempty" yaml:"allowedTools,omitempty"`
|
|
ExcludedTools []string `json:"excludedTools,omitempty" yaml:"excludedTools,omitempty"`
|
|
OAuthClientID string `json:"oauthClientId,omitempty" yaml:"oauthClientId,omitempty"`
|
|
OAuthClientSecret string `json:"oauthClientSecret,omitempty" yaml:"oauthClientSecret,omitempty"`
|
|
OAuthScopes []string `json:"oauthScopes,omitempty" yaml:"oauthScopes,omitempty"`
|
|
NoOAuth bool `json:"noOAuth,omitempty" yaml:"noOAuth,omitempty"`
|
|
TasksMode string `json:"tasksMode,omitempty" yaml:"tasksMode,omitempty"`
|
|
}
|
|
|
|
// Also try legacy format
|
|
type legacyFormat struct {
|
|
Transport string `json:"transport,omitempty"`
|
|
Command string `json:"command,omitempty"`
|
|
Args []string `json:"args,omitempty"`
|
|
Env map[string]any `json:"env,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
Headers []string `json:"headers,omitempty"`
|
|
AllowedTools []string `json:"allowedTools,omitempty" yaml:"allowedTools,omitempty"`
|
|
ExcludedTools []string `json:"excludedTools,omitempty" yaml:"excludedTools,omitempty"`
|
|
TasksMode string `json:"tasksMode,omitempty" yaml:"tasksMode,omitempty"`
|
|
}
|
|
|
|
// Try new format first
|
|
var newConfig newFormat
|
|
if err := json.Unmarshal(data, &newConfig); err == nil && newConfig.Type != "" {
|
|
s.Type = newConfig.Type
|
|
s.Command = newConfig.Command
|
|
s.Environment = newConfig.Environment
|
|
s.URL = newConfig.URL
|
|
s.Headers = newConfig.Headers
|
|
s.AllowedTools = newConfig.AllowedTools
|
|
s.ExcludedTools = newConfig.ExcludedTools
|
|
s.OAuthClientID = newConfig.OAuthClientID
|
|
s.OAuthClientSecret = newConfig.OAuthClientSecret
|
|
s.OAuthScopes = newConfig.OAuthScopes
|
|
s.NoOAuth = newConfig.NoOAuth
|
|
s.TasksMode = newConfig.TasksMode
|
|
return nil
|
|
}
|
|
|
|
// Fall back to legacy format
|
|
var legacyConfig legacyFormat
|
|
if err := json.Unmarshal(data, &legacyConfig); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Convert legacy format to new format
|
|
s.Transport = legacyConfig.Transport
|
|
if legacyConfig.Command != "" {
|
|
s.Command = append([]string{legacyConfig.Command}, legacyConfig.Args...)
|
|
}
|
|
s.Args = legacyConfig.Args
|
|
s.Env = legacyConfig.Env
|
|
s.URL = legacyConfig.URL
|
|
s.Headers = legacyConfig.Headers
|
|
s.AllowedTools = legacyConfig.AllowedTools
|
|
s.ExcludedTools = legacyConfig.ExcludedTools
|
|
s.TasksMode = legacyConfig.TasksMode
|
|
|
|
// Infer type from legacy format for better compatibility
|
|
// Only set Type when it doesn't change existing transport behavior
|
|
if legacyConfig.Command != "" {
|
|
s.Type = "local" // This maps to "stdio" which matches legacy behavior
|
|
}
|
|
// Don't set Type for URL-only configs to preserve legacy "sse" behavior
|
|
// The URL will be handled by the legacy fallback logic in GetTransportType()
|
|
|
|
return nil
|
|
}
|
|
|
|
// AdaptiveColor represents a color that adapts to light and dark themes.
|
|
// Either light or dark can be specified, or both for theme-aware coloring.
|
|
type AdaptiveColor struct {
|
|
Light string `json:"light,omitempty" yaml:"light,omitempty"`
|
|
Dark string `json:"dark,omitempty" yaml:"dark,omitempty"`
|
|
}
|
|
|
|
// MarkdownThemeConfig defines color overrides for markdown rendering and
|
|
// syntax highlighting.
|
|
type MarkdownThemeConfig struct {
|
|
Text AdaptiveColor `json:"text,omitzero" yaml:"text,omitempty"`
|
|
Muted AdaptiveColor `json:"muted,omitzero" yaml:"muted,omitempty"`
|
|
Heading AdaptiveColor `json:"heading,omitzero" yaml:"heading,omitempty"`
|
|
Emph AdaptiveColor `json:"emph,omitzero" yaml:"emph,omitempty"`
|
|
Strong AdaptiveColor `json:"strong,omitzero" yaml:"strong,omitempty"`
|
|
Link AdaptiveColor `json:"link,omitzero" yaml:"link,omitempty"`
|
|
Code AdaptiveColor `json:"code,omitzero" yaml:"code,omitempty"`
|
|
Error AdaptiveColor `json:"error,omitzero" yaml:"error,omitempty"`
|
|
Keyword AdaptiveColor `json:"keyword,omitzero" yaml:"keyword,omitempty"`
|
|
String AdaptiveColor `json:"string,omitzero" yaml:"string,omitempty"`
|
|
Number AdaptiveColor `json:"number,omitzero" yaml:"number,omitempty"`
|
|
Comment AdaptiveColor `json:"comment,omitzero" yaml:"comment,omitempty"`
|
|
}
|
|
|
|
// Theme defines the color scheme for the application UI with adaptive colors
|
|
// that support both light and dark modes.
|
|
type Theme struct {
|
|
Primary AdaptiveColor `json:"primary,omitzero" yaml:"primary,omitempty"`
|
|
Secondary AdaptiveColor `json:"secondary,omitzero" yaml:"secondary,omitempty"`
|
|
Success AdaptiveColor `json:"success,omitzero" yaml:"success,omitempty"`
|
|
Warning AdaptiveColor `json:"warning,omitzero" yaml:"warning,omitempty"`
|
|
Error AdaptiveColor `json:"error,omitzero" yaml:"error,omitempty"`
|
|
Info AdaptiveColor `json:"info,omitzero" yaml:"info,omitempty"`
|
|
Text AdaptiveColor `json:"text,omitzero" yaml:"text,omitempty"`
|
|
Muted AdaptiveColor `json:"muted,omitzero" yaml:"muted,omitempty"`
|
|
VeryMuted AdaptiveColor `json:"very-muted,omitzero" yaml:"very-muted,omitempty"`
|
|
Background AdaptiveColor `json:"background,omitzero" yaml:"background,omitempty"`
|
|
Border AdaptiveColor `json:"border,omitzero" yaml:"border,omitempty"`
|
|
MutedBorder AdaptiveColor `json:"muted-border,omitzero" yaml:"muted-border,omitempty"`
|
|
System AdaptiveColor `json:"system,omitzero" yaml:"system,omitempty"`
|
|
Tool AdaptiveColor `json:"tool,omitzero" yaml:"tool,omitempty"`
|
|
Accent AdaptiveColor `json:"accent,omitzero" yaml:"accent,omitempty"`
|
|
Highlight AdaptiveColor `json:"highlight,omitzero" yaml:"highlight,omitempty"`
|
|
|
|
// Diff block backgrounds
|
|
DiffInsertBg AdaptiveColor `json:"diff-insert-bg,omitzero" yaml:"diff-insert-bg,omitempty"`
|
|
DiffDeleteBg AdaptiveColor `json:"diff-delete-bg,omitzero" yaml:"diff-delete-bg,omitempty"`
|
|
DiffEqualBg AdaptiveColor `json:"diff-equal-bg,omitzero" yaml:"diff-equal-bg,omitempty"`
|
|
DiffMissingBg AdaptiveColor `json:"diff-missing-bg,omitzero" yaml:"diff-missing-bg,omitempty"`
|
|
|
|
// Code/output block backgrounds
|
|
CodeBg AdaptiveColor `json:"code-bg,omitzero" yaml:"code-bg,omitempty"`
|
|
GutterBg AdaptiveColor `json:"gutter-bg,omitzero" yaml:"gutter-bg,omitempty"`
|
|
WriteBg AdaptiveColor `json:"write-bg,omitzero" yaml:"write-bg,omitempty"`
|
|
|
|
// Markdown rendering and syntax highlighting
|
|
Markdown MarkdownThemeConfig `json:"markdown,omitzero" yaml:"markdown,omitempty"`
|
|
}
|
|
|
|
// GenerationParams defines generation parameter defaults that can be attached
|
|
// to individual models. These act as model-level defaults — CLI flags and
|
|
// global config values take precedence when explicitly set.
|
|
type GenerationParams struct {
|
|
MaxTokens *int `json:"maxTokens,omitempty" yaml:"maxTokens,omitempty"`
|
|
Temperature *float32 `json:"temperature,omitempty" yaml:"temperature,omitempty"`
|
|
TopP *float32 `json:"topP,omitempty" yaml:"topP,omitempty"`
|
|
TopK *int32 `json:"topK,omitempty" yaml:"topK,omitempty"`
|
|
FrequencyPenalty *float32 `json:"frequencyPenalty,omitempty" yaml:"frequencyPenalty,omitempty"`
|
|
PresencePenalty *float32 `json:"presencePenalty,omitempty" yaml:"presencePenalty,omitempty"`
|
|
StopSequences []string `json:"stopSequences,omitempty" yaml:"stopSequences,omitempty"`
|
|
ThinkingLevel string `json:"thinkingLevel,omitempty" yaml:"thinkingLevel,omitempty"`
|
|
SystemPrompt string `json:"systemPrompt,omitempty" yaml:"systemPrompt,omitempty"`
|
|
}
|
|
|
|
// CustomModelConfig defines a custom model that can be used with custom/custom
|
|
// or other custom/ prefixed models. These models are loaded from the config file
|
|
// and merged into the custom provider in the model registry.
|
|
type CustomModelConfig struct {
|
|
Name string `json:"name" yaml:"name"`
|
|
BaseURL string `json:"baseUrl,omitempty" yaml:"baseUrl,omitempty"`
|
|
APIKey string `json:"apiKey,omitempty" yaml:"apiKey,omitempty"`
|
|
Family string `json:"family,omitempty" yaml:"family,omitempty"`
|
|
Attachment bool `json:"attachment,omitempty" yaml:"attachment,omitempty"`
|
|
Reasoning bool `json:"reasoning,omitempty" yaml:"reasoning,omitempty"`
|
|
Temperature bool `json:"temperature,omitempty" yaml:"temperature,omitempty"`
|
|
Knowledge string `json:"knowledge,omitempty" yaml:"knowledge,omitempty"`
|
|
Cost CostConfig `json:"cost" yaml:"cost"`
|
|
Limit LimitConfig `json:"limit" yaml:"limit"`
|
|
|
|
// Generation parameter defaults for this model.
|
|
// These are applied when the user hasn't explicitly set the corresponding
|
|
// CLI flag or global config value.
|
|
Params GenerationParams `json:"params,omitzero" yaml:"params,omitempty"`
|
|
}
|
|
|
|
// CostConfig defines the pricing for a custom model.
|
|
type CostConfig struct {
|
|
Input float64 `json:"input" yaml:"input"`
|
|
Output float64 `json:"output" yaml:"output"`
|
|
}
|
|
|
|
// LimitConfig defines context and output limits for a custom model.
|
|
type LimitConfig struct {
|
|
Context int `json:"context" yaml:"context"`
|
|
Output int `json:"output" yaml:"output"`
|
|
}
|
|
|
|
// Config represents the complete application configuration including MCP servers,
|
|
// model settings, UI preferences, and API credentials. It supports both command-line
|
|
// flags and configuration file settings.
|
|
type Config struct {
|
|
MCPServers map[string]MCPServerConfig `json:"mcpServers" yaml:"mcpServers"`
|
|
Model string `json:"model,omitempty" yaml:"model,omitempty"`
|
|
MaxSteps int `json:"max-steps,omitempty" yaml:"max-steps,omitempty"`
|
|
Debug bool `json:"debug,omitempty" yaml:"debug,omitempty"`
|
|
SystemPrompt string `json:"system-prompt,omitempty" yaml:"system-prompt,omitempty"`
|
|
ProviderAPIKey string `json:"provider-api-key,omitempty" yaml:"provider-api-key,omitempty"`
|
|
ProviderURL string `json:"provider-url,omitempty" yaml:"provider-url,omitempty"`
|
|
Stream *bool `json:"stream,omitempty" yaml:"stream,omitempty"`
|
|
Theme any `json:"theme" yaml:"theme"`
|
|
// Model generation parameters
|
|
MaxTokens int `json:"max-tokens,omitempty" yaml:"max-tokens,omitempty"`
|
|
Temperature *float32 `json:"temperature,omitempty" yaml:"temperature,omitempty"`
|
|
TopP *float32 `json:"top-p,omitempty" yaml:"top-p,omitempty"`
|
|
TopK *int32 `json:"top-k,omitempty" yaml:"top-k,omitempty"`
|
|
FrequencyPenalty *float32 `json:"frequency-penalty,omitempty" yaml:"frequency-penalty,omitempty"`
|
|
PresencePenalty *float32 `json:"presence-penalty,omitempty" yaml:"presence-penalty,omitempty"`
|
|
StopSequences []string `json:"stop-sequences,omitempty" yaml:"stop-sequences,omitempty"`
|
|
|
|
// Thinking / extended reasoning
|
|
ThinkingLevel string `json:"thinking-level,omitempty" yaml:"thinking-level,omitempty"`
|
|
|
|
// TLS configuration
|
|
TLSSkipVerify bool `json:"tls-skip-verify,omitempty" yaml:"tls-skip-verify,omitempty"`
|
|
|
|
// Prompt templates configuration
|
|
Prompts []string `json:"prompts,omitempty" yaml:"prompts,omitempty"`
|
|
NoPromptTemplates bool `json:"no-prompt-templates,omitempty" yaml:"no-prompt-templates,omitempty"`
|
|
|
|
// Custom model definitions (under custom/ provider)
|
|
CustomModels map[string]CustomModelConfig `json:"customModels,omitempty" yaml:"customModels,omitempty"`
|
|
|
|
// Per-model generation parameter overrides. Keys are "provider/model" strings
|
|
// (e.g. "anthropic/claude-sonnet-4-5-20250929", "openai/gpt-4o"). These
|
|
// settings act as model-level defaults — CLI flags and global config values
|
|
// take precedence when explicitly set.
|
|
ModelSettings map[string]GenerationParams `json:"modelSettings,omitempty" yaml:"modelSettings,omitempty"`
|
|
}
|
|
|
|
// GetTransportType returns the transport type for the server config, mapping
|
|
// simplified type names to actual transport protocols. Supports legacy format
|
|
// detection and automatic type inference from configuration.
|
|
func (s *MCPServerConfig) GetTransportType() string {
|
|
// Legacy format support - check explicit transport first
|
|
if s.Transport != "" {
|
|
return s.Transport
|
|
}
|
|
|
|
// New simplified format
|
|
if s.Type != "" {
|
|
switch s.Type {
|
|
case "local":
|
|
return "stdio"
|
|
case "remote":
|
|
return "streamable"
|
|
case "inprocess":
|
|
return "inprocess"
|
|
default:
|
|
return s.Type
|
|
}
|
|
}
|
|
|
|
// Programmatic in-process server detection.
|
|
if s.InProcessServer != nil {
|
|
return "inprocess"
|
|
}
|
|
|
|
// Backward compatibility: infer transport type
|
|
if len(s.Command) > 0 {
|
|
return "stdio"
|
|
}
|
|
if s.URL != "" {
|
|
return "sse"
|
|
}
|
|
return "stdio" // default
|
|
}
|
|
|
|
// Validate validates the configuration, ensuring required fields are present
|
|
// for each server type and that tool filters are used correctly. Returns an
|
|
// error describing any validation failures.
|
|
func (c *Config) Validate() error {
|
|
for serverName, serverConfig := range c.MCPServers {
|
|
if len(serverConfig.AllowedTools) > 0 && len(serverConfig.ExcludedTools) > 0 {
|
|
return fmt.Errorf("server %s: allowedTools and excludedTools are mutually exclusive", serverName)
|
|
}
|
|
|
|
// Reject unknown tasksMode values up front so a typo (e.g. "alwasy")
|
|
// fails loud here instead of being silently downgraded to "auto" by
|
|
// the runtime parser. Comparison is case-insensitive to match
|
|
// tools.ParseTaskMode.
|
|
switch strings.ToLower(strings.TrimSpace(serverConfig.TasksMode)) {
|
|
case "", "auto", "never", "always":
|
|
// ok
|
|
default:
|
|
return fmt.Errorf("server %s: invalid tasksMode %q (expected one of: auto, never, always)", serverName, serverConfig.TasksMode)
|
|
}
|
|
|
|
transport := serverConfig.GetTransportType()
|
|
switch transport {
|
|
case "stdio":
|
|
// Check both new and legacy command formats
|
|
if len(serverConfig.Command) == 0 && serverConfig.Transport == "" {
|
|
return fmt.Errorf("server %s: command is required for stdio transport", serverName)
|
|
}
|
|
case "sse", "streamable":
|
|
if serverConfig.URL == "" {
|
|
return fmt.Errorf("server %s: url is required for %s transport", serverName, transport)
|
|
}
|
|
case "inprocess":
|
|
if serverConfig.InProcessServer == nil {
|
|
return fmt.Errorf("server %s: InProcessServer is required for inprocess transport", serverName)
|
|
}
|
|
default:
|
|
return fmt.Errorf("server %s: unsupported transport type '%s'. Supported types: stdio, sse, streamable, inprocess", serverName, transport)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LoadSystemPrompt loads system prompt from file or returns the string directly.
|
|
// If input is a path to an existing file, its contents are read and returned.
|
|
// Otherwise, the input string is returned as-is.
|
|
func LoadSystemPrompt(input string) (string, error) {
|
|
if input == "" {
|
|
return "", nil
|
|
}
|
|
|
|
// Check if input is a file that exists
|
|
if _, err := os.Stat(input); err == nil {
|
|
// Read the entire file as plain text
|
|
content, err := os.ReadFile(input)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error reading system prompt file: %v", err)
|
|
}
|
|
return strings.TrimSpace(string(content)), nil
|
|
}
|
|
|
|
// Treat as direct string
|
|
return input, nil
|
|
}
|
|
|
|
// EnsureConfigExists checks if a config file exists and creates a default one if not.
|
|
// It searches for .kit.{yml,yaml,json} files in the user's home directory.
|
|
// If none exist, creates a default .kit.yml with examples.
|
|
func EnsureConfigExists() error {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return fmt.Errorf("error getting home directory: %v", err)
|
|
}
|
|
|
|
// Check for existing config files
|
|
configNames := []string{".kit"}
|
|
configTypes := []string{"yml", "yaml", "json"}
|
|
|
|
for _, configName := range configNames {
|
|
for _, configType := range configTypes {
|
|
configPath := filepath.Join(homeDir, configName+"."+configType)
|
|
if _, err := os.Stat(configPath); err == nil {
|
|
// Config file exists, no need to create
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// No config file found, create default
|
|
return createDefaultConfig(homeDir)
|
|
}
|
|
|
|
// createDefaultConfig creates a default .kit.yml file in the user's home directory
|
|
func createDefaultConfig(homeDir string) error {
|
|
configPath := filepath.Join(homeDir, ".kit.yml")
|
|
|
|
// Create the file
|
|
file, err := os.Create(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating config file: %v", err)
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
// Write a comprehensive YAML template with examples
|
|
content := `# KIT Configuration File
|
|
# All command-line flags can be configured here
|
|
|
|
# MCP Servers configuration (for external tool servers)
|
|
# Core tools (bash, read, write, edit, grep, find, ls) are built-in and always available.
|
|
# Add external MCP servers here for additional tools:
|
|
# mcpServers:
|
|
# # Local MCP servers - run commands locally via stdio transport
|
|
# filesystem:
|
|
# type: "local"
|
|
# command: ["npx", "@modelcontextprotocol/server-filesystem", "/tmp"]
|
|
# environment:
|
|
# DEBUG: "true"
|
|
#
|
|
# # Remote MCP servers - connect via StreamableHTTP transport
|
|
# websearch:
|
|
# type: "remote"
|
|
# url: "https://api.example.com/mcp"
|
|
|
|
mcpServers:
|
|
|
|
# Application settings (all optional)
|
|
# model: "anthropic/claude-sonnet-4-5-20250929" # Default model to use
|
|
# max-steps: 10 # Maximum agent steps (0 for unlimited)
|
|
# debug: false # Enable debug logging
|
|
# system-prompt: "/path/to/system-prompt.txt" # System prompt text file
|
|
|
|
# Model generation parameters (all optional, apply globally to all models)
|
|
# max-tokens: 4096 # Maximum tokens in response
|
|
# temperature: 0.7 # Randomness (0.0-1.0)
|
|
# top-p: 0.95 # Nucleus sampling (0.0-1.0)
|
|
# top-k: 40 # Top K sampling
|
|
# frequency-penalty: 0.0 # Penalize frequent tokens (0.0-2.0)
|
|
# presence-penalty: 0.0 # Penalize present tokens (0.0-2.0)
|
|
# stop-sequences: ["Human:", "Assistant:"] # Custom stop sequences
|
|
|
|
# Per-model generation parameter overrides (apply to specific models)
|
|
# These act as model-level defaults — CLI flags and global settings above take precedence.
|
|
# Keys are "provider/model" strings matching the model you use.
|
|
# modelSettings:
|
|
# anthropic/claude-sonnet-4-5-20250929:
|
|
# temperature: 0.3
|
|
# maxTokens: 8192
|
|
# openai/gpt-4o:
|
|
# temperature: 0.7
|
|
# topP: 0.95
|
|
# topK: 40
|
|
# frequencyPenalty: 0.1
|
|
# presencePenalty: 0.1
|
|
# anthropic/claude-opus-4-6:
|
|
# thinkingLevel: "high"
|
|
# maxTokens: 16384
|
|
# systemPrompt: "You are a deep reasoning assistant." # or a file path
|
|
|
|
# Skills configuration (all optional)
|
|
# no-skills: false # Set to true to disable all skill loading
|
|
# skill: # Explicit skill files/dirs (disables auto-discovery)
|
|
# - "/path/to/skill.md"
|
|
# skills-dir: "/path/to/skills" # Override project-local directory for auto-discovery
|
|
|
|
# API Configuration (can also use environment variables)
|
|
# provider-api-key: "your-api-key" # API key for OpenAI, Anthropic, or Google
|
|
# provider-url: "https://api.openai.com/v1" # Base URL for OpenAI, Anthropic, or Ollama
|
|
|
|
# Custom model definitions (under custom/ provider)
|
|
# customModels:
|
|
# my-local-llama:
|
|
# name: "Local Llama 3"
|
|
# baseUrl: "http://localhost:8080/v1"
|
|
# family: "llama"
|
|
# temperature: true
|
|
# cost:
|
|
# input: 0.0
|
|
# output: 0.0
|
|
# limit:
|
|
# context: 131072
|
|
# output: 8192
|
|
# params: # Generation parameter defaults for this model
|
|
# temperature: 0.8
|
|
# topP: 0.95
|
|
# topK: 40
|
|
# systemPrompt: "You are a helpful local assistant."
|
|
`
|
|
|
|
_, err = file.WriteString(content)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing config content: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// FilepathOr reads a configuration value that can be either a direct value or a
|
|
// filepath to a JSON/YAML file containing the value. If the value is a string
|
|
// starting with "~/" or a relative path, it's expanded to an absolute path.
|
|
// The contents of the file are then unmarshaled into the provided value pointer.
|
|
func FilepathOr[T any](key string, value *T) error {
|
|
var field any
|
|
err := viper.UnmarshalKey(key, &field)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch f := field.(type) {
|
|
case string:
|
|
{
|
|
absPath := f
|
|
if strings.HasPrefix(absPath, "~/") {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
absPath = filepath.Join(home, absPath[2:])
|
|
}
|
|
if !filepath.IsAbs(absPath) {
|
|
base := configPath
|
|
if base == "" {
|
|
fmt.Fprintf(os.Stderr, "unable to build relative path to config.")
|
|
os.Exit(1)
|
|
}
|
|
absPath = filepath.Join(filepath.Dir(base), absPath)
|
|
}
|
|
b, err := os.ReadFile(absPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%q", err)
|
|
os.Exit(1)
|
|
}
|
|
switch filepath.Ext(absPath) {
|
|
case ".json":
|
|
return json.Unmarshal(b, value)
|
|
case ".yaml", ".yml":
|
|
return yaml.Unmarshal(b, value)
|
|
}
|
|
}
|
|
case map[string]any:
|
|
return viper.UnmarshalKey(key, value)
|
|
default:
|
|
return fmt.Errorf("invalid type for field %q", key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var configPath string
|
|
|
|
// SetConfigPath sets the configuration file path for resolving relative paths
|
|
// in configuration values. This should be called when the configuration file
|
|
// location is known.
|
|
func SetConfigPath(path string) {
|
|
configPath = path
|
|
}
|