package commandkit import ( "bytes" "context" "errors" "fmt" "strconv" "strings" configkit "gitea.maximumdirect.net/eric/ConfigKit" "gitea.maximumdirect.net/eric/promptkit" ) // PromptKitSuite declares optional PromptKit prompt and profile inspection // commands for an App. type PromptKitSuite[C any] struct { // NewContext creates command-local application context when an inspection runs. NewContext func() C // Selectors declares context flags accepted by the installed commands. Each // explicit scope list must include "promptkit"; an empty list applies here. Selectors []Selector[C] // CheckInput validates standard configuration selections and selector values. CheckInput func(Selection, C) error // InspectPrompt inspects an ID and exact or omitted version. InspectPrompt func(context.Context, Selection, C, string, string) (Acquired[*promptkit.PromptInspection], error) // InspectProfile inspects one profile ID. InspectProfile func(context.Context, Selection, C, string) (Acquired[*promptkit.ProfileInspection], error) } // NewPromptKitSuite creates prompt and profile inspection callbacks backed by // a PromptKit Engine loader. The loader runs once per selected command. Each // callback captures configuration diagnostics before calling the corresponding // PromptKit inspection method, and returns those diagnostics with lookup errors. func NewPromptKitSuite[C any]( load func(context.Context, Selection, C) (*promptkit.Engine, error), ) (PromptKitSuite[C], error) { if load == nil { return PromptKitSuite[C]{}, fmt.Errorf("PromptKit engine loader is nil") } return PromptKitSuite[C]{ InspectPrompt: func(ctx context.Context, selection Selection, appContext C, id, version string) (Acquired[*promptkit.PromptInspection], error) { engine, _, diagnostics, err := loadPromptKitInspection(ctx, selection, appContext, load) if err != nil { return Acquired[*promptkit.PromptInspection]{Diagnostics: diagnostics}, err } inspection, err := engine.InspectPrompt(ctx, id, version) return Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: diagnostics}, err }, InspectProfile: func(ctx context.Context, selection Selection, appContext C, id string) (Acquired[*promptkit.ProfileInspection], error) { engine, _, diagnostics, err := loadPromptKitInspection(ctx, selection, appContext, load) if err != nil { return Acquired[*promptkit.ProfileInspection]{Diagnostics: diagnostics}, err } inspection, err := engine.InspectProfile(ctx, id) return Acquired[*promptkit.ProfileInspection]{Value: inspection, Diagnostics: diagnostics}, err }, }, nil } func loadPromptKitInspection[C any](ctx context.Context, selection Selection, appContext C, load func(context.Context, Selection, C) (*promptkit.Engine, error)) (*promptkit.Engine, promptkit.ConfigurationInspection, []Diagnostic, error) { engine, err := load(ctx, selection, appContext) if err != nil { var loadErr *configkit.LoadError if errors.As(err, &loadErr) { return nil, promptkit.ConfigurationInspection{}, ConfigKitDiagnostics("promptkit", loadErr.Sources(), loadErr.Diagnostics()), err } return nil, promptkit.ConfigurationInspection{}, nil, err } if engine == nil { return nil, promptkit.ConfigurationInspection{}, nil, fmt.Errorf("PromptKit configuration loader returned a nil engine") } if ctx != nil { if err := ctx.Err(); err != nil { return nil, promptkit.ConfigurationInspection{}, nil, err } } inspection, err := engine.InspectConfiguration() if err != nil { return nil, promptkit.ConfigurationInspection{}, nil, err } sources := inspection.Sources() diagnostics := ConfigKitDiagnostics("promptkit", sources, inspection.Diagnostics()) return engine, inspection, diagnostics, nil } // RegisterPromptKit installs the supplied PromptKit inspection commands under // promptkit show. Each nonnil callback installs only its matching command. A // suite with no inspection callbacks installs no PromptKit namespace. func RegisterPromptKit[C any](app *App, suite PromptKitSuite[C]) error { if suite.InspectPrompt == nil && suite.InspectProfile == nil { return nil } if app == nil { return fmt.Errorf("cannot register PromptKit commands on a nil app") } normalized, err := normalizePromptKitSuite(suite) if err != nil { return err } showCommands := make([]Command, 0, 2) if normalized.InspectPrompt != nil { command, err := newPromptKitPromptCommand(normalized) if err != nil { return err } showCommands = append(showCommands, command) } if normalized.InspectProfile != nil { command, err := newPromptKitProfileCommand(normalized) if err != nil { return err } showCommands = append(showCommands, command) } show, err := Group("show", "Inspect PromptKit definitions.", showCommands...) if err != nil { return err } root, err := Group("promptkit", "Inspect PromptKit definitions.", show) if err != nil { return err } return app.Register(root) } func normalizePromptKitSuite[C any](suite PromptKitSuite[C]) (PromptKitSuite[C], error) { normalized := suite normalized.Selectors = append([]Selector[C](nil), suite.Selectors...) standardNames := map[string]bool{"help": true} for _, flag := range promptKitInputFlags(PromptKitSuite[C]{}, suite.InspectPrompt != nil) { standardNames[flag.binding.meta.Name] = true } seenNames := make(map[string]struct{}, len(normalized.Selectors)) seenShorts := make(map[string]struct{}, len(normalized.Selectors)) for i, selector := range normalized.Selectors { binding := selector.Binding.binding meta := binding.meta if !binding.valid || binding.clear == nil || meta.Name == "" { return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector %d has an invalid flag binding", i) } if !validFlagName(meta.Name) { return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector flag name %q is invalid", meta.Name) } if standardNames[meta.Name] { return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector flag --%s conflicts with a standard flag", meta.Name) } if meta.Short == "h" { return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector short flag -%s conflicts with help", meta.Short) } if meta.Short != "" && (len(meta.Short) != 1 || meta.Short == "-") { return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector short spelling %q must be one byte", meta.Short) } if _, exists := seenNames[meta.Name]; exists { return PromptKitSuite[C]{}, fmt.Errorf("duplicate PromptKit selector flag --%s", meta.Name) } seenNames[meta.Name] = struct{}{} if meta.Short != "" { if _, exists := seenShorts[meta.Short]; exists { return PromptKitSuite[C]{}, fmt.Errorf("duplicate PromptKit selector short flag -%s", meta.Short) } seenShorts[meta.Short] = struct{}{} } applies := len(selector.Scopes) == 0 for _, scope := range selector.Scopes { if scope == "promptkit" { applies = true break } } if !applies { return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector --%s must apply to scope %q", meta.Name, "promptkit") } normalized.Selectors[i].Scopes = append([]string(nil), selector.Scopes...) } return normalized, nil } type promptKitInput[C any] struct { Selection Selection Format string ID string Version string SelectorContext C selectors []selectorState[C] } func promptKitInputFlags[C any](suite PromptKitSuite[C], prompt bool) []Flag[promptKitInput[C]] { formatDefault := "text" flags := []Flag[promptKitInput[C]]{ ValueFlag[promptKitInput[C]](FlagMeta{Name: "format", Usage: "Output format."}, &formatDefault, parseString, func(input *promptKitInput[C], value Supplied[string]) { input.Format = value.Value }), } flags = append(flags, configurationSelectionFlags( func(input *promptKitInput[C], value Supplied[string]) { input.Selection.Config = value }, func(input *promptKitInput[C], value Supplied[string]) { input.Selection.Overlay = value }, )...) if prompt { versionDefault := "" flags = append(flags, ValueFlag[promptKitInput[C]](FlagMeta{Name: "version", Usage: "Exact prompt version; empty selects the owning default."}, &versionDefault, parseString, func(input *promptKitInput[C], value Supplied[string]) { input.Version = value.Value })) } for _, selector := range suite.Selectors { flags = append(flags, adaptContextSelector(selector, func(input *promptKitInput[C]) *C { return &input.SelectorContext }, func(input *promptKitInput[C], state selectorState[C]) { input.selectors = append(input.selectors, state) }, )) } return flags } func checkPromptKitInput[C any](suite PromptKitSuite[C], input promptKitInput[C]) error { if _, err := parseConfigFormat(input.Format); err != nil { return err } if suite.CheckInput != nil { return suite.CheckInput(input.Selection, input.SelectorContext) } return nil } func promptKitContext[C any](suite PromptKitSuite[C], input promptKitInput[C]) C { var result C if suite.NewContext != nil { result = suite.NewContext() } for _, selector := range input.selectors { if selectorApplies(selector.scopes, "promptkit") { selector.apply(&result) } else { selector.clear(&result) } } return result } func newPromptKitPromptCommand[C any](suite PromptKitSuite[C]) (Command, error) { inputSpec := CommandSpec[promptKitInput[C]]{ Name: "prompt", Summary: "Inspect one PromptKit prompt definition.", Flags: promptKitInputFlags(suite, true), Arguments: []Argument[promptKitInput[C]]{ValueArgument[promptKitInput[C]](ArgumentMeta{Name: "ID"}, true, parseString, func(input *promptKitInput[C], value Supplied[string]) { input.ID = value.Value })}, CheckInput: func(input promptKitInput[C]) error { return checkPromptKitInput(suite, input) }, Run: func(invocation Invocation, input promptKitInput[C]) Outcome { return runPromptKitPrompt(invocation, suite, input) }, } return NewCommand(inputSpec) } func newPromptKitProfileCommand[C any](suite PromptKitSuite[C]) (Command, error) { inputSpec := CommandSpec[promptKitInput[C]]{ Name: "profile", Summary: "Inspect one PromptKit profile definition.", Flags: promptKitInputFlags(suite, false), Arguments: []Argument[promptKitInput[C]]{ValueArgument[promptKitInput[C]](ArgumentMeta{Name: "ID"}, true, parseString, func(input *promptKitInput[C], value Supplied[string]) { input.ID = value.Value })}, CheckInput: func(input promptKitInput[C]) error { return checkPromptKitInput(suite, input) }, Run: func(invocation Invocation, input promptKitInput[C]) Outcome { return runPromptKitProfile(invocation, suite, input) }, } return NewCommand(inputSpec) } func runPromptKitPrompt[C any](invocation Invocation, suite PromptKitSuite[C], input promptKitInput[C]) Outcome { if err := invocation.Context.Err(); err != nil { return Failure(err) } contextValue := promptKitContext(suite, input) if err := invocation.Context.Err(); err != nil { return Failure(err) } acquired, err := suite.InspectPrompt(invocation.Context, input.Selection, contextValue, input.ID, input.Version) if err != nil { return promptKitFailure(err, acquired.Diagnostics) } if err := invocation.Context.Err(); err != nil { return promptKitFailure(err, acquired.Diagnostics) } if acquired.Value == nil { return promptKitFailure(fmt.Errorf("PromptKit prompt inspection returned no result"), acquired.Diagnostics) } report := promptReportFromInspection(acquired.Value) var output []byte if input.Format == "yaml" { var buffer bytes.Buffer if err := encodeYAML(&buffer, report); err != nil { return promptKitFailure(err, acquired.Diagnostics) } output = buffer.Bytes() } else { output = []byte(formatPromptText(report)) } return writePromptKitReport(invocation, output, acquired.Diagnostics) } func runPromptKitProfile[C any](invocation Invocation, suite PromptKitSuite[C], input promptKitInput[C]) Outcome { if err := invocation.Context.Err(); err != nil { return Failure(err) } contextValue := promptKitContext(suite, input) if err := invocation.Context.Err(); err != nil { return Failure(err) } acquired, err := suite.InspectProfile(invocation.Context, input.Selection, contextValue, input.ID) if err != nil { return promptKitFailure(err, acquired.Diagnostics) } if err := invocation.Context.Err(); err != nil { return promptKitFailure(err, acquired.Diagnostics) } if acquired.Value == nil { return promptKitFailure(fmt.Errorf("PromptKit profile inspection returned no result"), acquired.Diagnostics) } report := profileReportFromInspection(acquired.Value) var output []byte if input.Format == "yaml" { var buffer bytes.Buffer if err := encodeYAML(&buffer, report); err != nil { return promptKitFailure(err, acquired.Diagnostics) } output = buffer.Bytes() } else { textReport, err := formatProfileText(report) if err != nil { return promptKitFailure(err, acquired.Diagnostics) } output = []byte(textReport) } return writePromptKitReport(invocation, output, acquired.Diagnostics) } func promptKitFailure(err error, diagnostics []Diagnostic) Outcome { outcome := Failure(err) outcome.Diagnostics = diagnostics return outcome } func writePromptKitReport(invocation Invocation, output []byte, diagnostics []Diagnostic) Outcome { if err := invocation.Context.Err(); err != nil { return promptKitFailure(err, diagnostics) } if err := writeReport(invocation.Streams.Out, output); err != nil { outcome := incompleteReport(err) outcome.Diagnostics = diagnostics return outcome } outcome := Success() outcome.Diagnostics = diagnostics return outcome } type promptInputReport struct { Name string `yaml:"name"` Required bool `yaml:"required"` ContentType string `yaml:"content_type"` Description string `yaml:"description"` } type promptOutputContractReport struct { Format string `yaml:"format"` ValidationMode string `yaml:"validation_mode"` SchemaPath string `yaml:"schema_path"` RepairAttempts int `yaml:"repair_attempts"` } type promptInspectionReport struct { PromptID string `yaml:"prompt_id"` PromptVersion string `yaml:"prompt_version"` PromptHash string `yaml:"prompt_hash"` DefaultProfileID string `yaml:"default_profile_id"` Inputs []promptInputReport `yaml:"inputs"` OutputContract promptOutputContractReport `yaml:"output_contract"` } func promptReportFromInspection(inspection *promptkit.PromptInspection) promptInspectionReport { report := promptInspectionReport{ PromptID: inspection.PromptID, PromptVersion: inspection.PromptVersion, PromptHash: inspection.PromptHash, DefaultProfileID: inspection.DefaultProfileID, Inputs: make([]promptInputReport, 0, len(inspection.Inputs)), OutputContract: promptOutputContractReport{ Format: string(inspection.OutputContract.Format), ValidationMode: string(inspection.OutputContract.ValidationMode), SchemaPath: inspection.OutputContract.SchemaPath, RepairAttempts: inspection.OutputContract.RepairAttempts, }, } for _, input := range inspection.Inputs { report.Inputs = append(report.Inputs, promptInputReport{Name: input.Name, Required: input.Required, ContentType: input.ContentType, Description: input.Description}) } return report } type modelParamsReport struct { BackendID string `yaml:"backend_id"` Endpoint string `yaml:"endpoint"` Model string `yaml:"model"` Temperature float64 `yaml:"temperature"` MaxTokens int `yaml:"max_tokens"` TopP float64 `yaml:"top_p"` TimeoutSeconds int `yaml:"timeout_seconds"` ServiceTier string `yaml:"service_tier"` ReasoningEffort string `yaml:"reasoning_effort"` APIKeyEnv string `yaml:"api_key_env"` ExtraParams map[string]any `yaml:"extra_params"` } type profileInspectionReport struct { ProfileID string `yaml:"profile_id"` EffectiveModelParams modelParamsReport `yaml:"effective_model_params"` APIKeyRequired bool `yaml:"api_key_required"` } func profileReportFromInspection(inspection *promptkit.ProfileInspection) profileInspectionReport { params := inspection.EffectiveModelParams extra := params.ExtraParams if extra == nil { extra = map[string]any{} } return profileInspectionReport{ ProfileID: inspection.ProfileID, EffectiveModelParams: modelParamsReport{ BackendID: params.BackendID, Endpoint: params.Endpoint, Model: params.Model, Temperature: params.Temperature, MaxTokens: params.MaxTokens, TopP: params.TopP, TimeoutSeconds: params.TimeoutSeconds, ServiceTier: params.ServiceTier, ReasoningEffort: params.ReasoningEffort, APIKeyEnv: params.APIKeyEnv, ExtraParams: extra, }, APIKeyRequired: inspection.APIKeyRequired, } } func formatPromptText(report promptInspectionReport) string { var output strings.Builder fmt.Fprintf(&output, "Prompt ID: %s\n", strconv.Quote(report.PromptID)) fmt.Fprintf(&output, "Prompt version: %s\n", strconv.Quote(report.PromptVersion)) fmt.Fprintf(&output, "Prompt hash: %s\n", strconv.Quote(report.PromptHash)) fmt.Fprintf(&output, "Default profile ID: %s\n", strconv.Quote(report.DefaultProfileID)) output.WriteString("Inputs:\n") for _, input := range report.Inputs { fmt.Fprintf(&output, " - name: %s\n required: %t\n content_type: %s\n description: %s\n", strconv.Quote(input.Name), input.Required, strconv.Quote(input.ContentType), strconv.Quote(input.Description)) } if len(report.Inputs) == 0 { output.WriteString(" []\n") } fmt.Fprintf(&output, "Output contract:\n format: %s\n validation_mode: %s\n schema_path: %s\n repair_attempts: %d\n", strconv.Quote(report.OutputContract.Format), strconv.Quote(report.OutputContract.ValidationMode), strconv.Quote(report.OutputContract.SchemaPath), report.OutputContract.RepairAttempts) return output.String() } func formatProfileText(report profileInspectionReport) (string, error) { params := report.EffectiveModelParams var output strings.Builder fmt.Fprintf(&output, "Profile ID: %s\n", strconv.Quote(report.ProfileID)) output.WriteString("Effective model params:\n") fmt.Fprintf(&output, " backend_id: %s\n endpoint: %s\n model: %s\n temperature: %s\n max_tokens: %d\n top_p: %s\n timeout_seconds: %d\n service_tier: %s\n reasoning_effort: %s\n api_key_env: %s\n", strconv.Quote(params.BackendID), strconv.Quote(params.Endpoint), strconv.Quote(params.Model), strconv.FormatFloat(params.Temperature, 'g', -1, 64), params.MaxTokens, strconv.FormatFloat(params.TopP, 'g', -1, 64), params.TimeoutSeconds, strconv.Quote(params.ServiceTier), strconv.Quote(params.ReasoningEffort), strconv.Quote(params.APIKeyEnv)) output.WriteString(" extra_params:\n") var nested strings.Builder if err := encodeYAML(&nested, params.ExtraParams); err != nil { return "", err } for _, line := range strings.Split(strings.TrimSuffix(nested.String(), "\n"), "\n") { output.WriteString(" ") output.WriteString(line) output.WriteByte('\n') } fmt.Fprintf(&output, "API key required: %t\n", report.APIKeyRequired) return output.String(), nil }