Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
360 lines
12 KiB
Go
360 lines
12 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
tuttigenerated "github.com/tutti-os/tutti/services/tuttid/api/generated"
|
|
"github.com/tutti-os/tutti/services/tuttid/apierrors"
|
|
cliservice "github.com/tutti-os/tutti/services/tuttid/service/cli"
|
|
)
|
|
|
|
func (api DaemonAPI) ListCliCapabilities(ctx context.Context, request tuttigenerated.ListCliCapabilitiesRequestObject) (tuttigenerated.ListCliCapabilitiesResponseObject, error) {
|
|
if api.CLIRegistry == nil {
|
|
return tuttigenerated.ListCliCapabilities503JSONResponse{
|
|
ServiceUnavailableErrorJSONResponse: serviceUnavailableError(
|
|
apierrors.ServiceUnavailable("cli_registry_unavailable", apierrors.WithDeveloperMessage("cli registry is unavailable")),
|
|
),
|
|
}, nil
|
|
}
|
|
|
|
workspaceID := ""
|
|
if request.Params.WorkspaceID != nil {
|
|
workspaceID = *request.Params.WorkspaceID
|
|
}
|
|
agentSessionID := ""
|
|
if request.Params.AgentSessionID != nil {
|
|
agentSessionID = *request.Params.AgentSessionID
|
|
}
|
|
includeHidden := request.Params.IncludeHidden != nil && *request.Params.IncludeHidden
|
|
includeIntegration := request.Params.IncludeIntegration != nil && *request.Params.IncludeIntegration
|
|
capabilities := api.CLIRegistry.Capabilities(ctx, cliservice.InvokeContext{
|
|
Source: "cli",
|
|
WorkspaceID: workspaceID,
|
|
AgentSessionID: agentSessionID,
|
|
SkipCapabilityFilters: includeHidden,
|
|
IncludeIntegrationCapabilities: includeHidden || includeIntegration,
|
|
})
|
|
return tuttigenerated.ListCliCapabilities200JSONResponse{
|
|
Commands: generatedCliCapabilities(capabilities),
|
|
}, nil
|
|
}
|
|
|
|
func (api DaemonAPI) InvokeCliCommand(ctx context.Context, request tuttigenerated.InvokeCliCommandRequestObject) (tuttigenerated.InvokeCliCommandResponseObject, error) {
|
|
if api.CLIRegistry == nil {
|
|
return tuttigenerated.InvokeCliCommand503JSONResponse{
|
|
ServiceUnavailableErrorJSONResponse: serviceUnavailableError(
|
|
apierrors.ServiceUnavailable("cli_registry_unavailable", apierrors.WithDeveloperMessage("cli registry is unavailable")),
|
|
),
|
|
}, nil
|
|
}
|
|
if request.Body == nil {
|
|
return tuttigenerated.InvokeCliCommand400JSONResponse{
|
|
InvalidRequestErrorJSONResponse: invalidRequestError(apierrors.EmptyBody(apierrors.WithDeveloperMessage("empty body"))),
|
|
}, nil
|
|
}
|
|
|
|
output, err := api.CLIRegistry.Invoke(ctx, cliservice.InvokeRequest{
|
|
CommandID: request.CommandID,
|
|
Input: generatedCliInput(request.Body.Input),
|
|
OutputMode: serviceCliOutputMode(request.Body.OutputMode),
|
|
Context: serviceCliContext(request.Body.Context),
|
|
})
|
|
if err != nil {
|
|
return writeInvokeCliCommandError(err), nil
|
|
}
|
|
return tuttigenerated.InvokeCliCommand200JSONResponse{
|
|
Ok: true,
|
|
Output: generatedCliCommandOutput(output),
|
|
}, nil
|
|
}
|
|
|
|
func writeInvokeCliCommandError(err error) tuttigenerated.InvokeCliCommandResponseObject {
|
|
if errors.Is(err, cliservice.ErrCommandNotFound) {
|
|
return tuttigenerated.InvokeCliCommand404JSONResponse(protocolErrorResponse(
|
|
apierrors.InvalidRequest("cli_command_not_found", apierrors.WithDeveloperMessage("cli command not found")),
|
|
))
|
|
}
|
|
if errors.Is(err, cliservice.ErrInvalidInput) {
|
|
reason := cliservice.InvokeErrorReason(err)
|
|
if reason != "" {
|
|
return tuttigenerated.InvokeCliCommand400JSONResponse{
|
|
InvalidRequestErrorJSONResponse: invalidRequestError(
|
|
apierrors.InvalidRequest(reason, apierrors.WithCause(err)),
|
|
),
|
|
}
|
|
}
|
|
return tuttigenerated.InvokeCliCommand400JSONResponse{
|
|
InvalidRequestErrorJSONResponse: invalidRequestError(
|
|
apierrors.MalformedRequest(apierrors.WithCause(err)),
|
|
),
|
|
}
|
|
}
|
|
if errors.Is(err, cliservice.ErrServiceUnavailable) {
|
|
reason := cliservice.InvokeErrorReason(err)
|
|
if reason == "" {
|
|
reason = "cli_service_unavailable"
|
|
}
|
|
return tuttigenerated.InvokeCliCommand503JSONResponse{
|
|
ServiceUnavailableErrorJSONResponse: serviceUnavailableError(
|
|
apierrors.ServiceUnavailable(reason, apierrors.WithCause(err)),
|
|
),
|
|
}
|
|
}
|
|
if errors.Is(err, cliservice.ErrWorkspaceOperation) {
|
|
return tuttigenerated.InvokeCliCommand502JSONResponse{
|
|
WorkspaceOperationErrorJSONResponse: workspaceOperationError(
|
|
apierrors.WorkspaceOperationFailed(apierrors.WithCause(err)),
|
|
),
|
|
}
|
|
}
|
|
protocolErr := apierrors.Classify(err)
|
|
switch protocolErr.Code {
|
|
case tuttigenerated.InvalidRequest:
|
|
return tuttigenerated.InvokeCliCommand400JSONResponse{
|
|
InvalidRequestErrorJSONResponse: invalidRequestError(protocolErr),
|
|
}
|
|
case tuttigenerated.ServiceUnavailable:
|
|
return tuttigenerated.InvokeCliCommand503JSONResponse{
|
|
ServiceUnavailableErrorJSONResponse: serviceUnavailableError(protocolErr),
|
|
}
|
|
default:
|
|
return tuttigenerated.InvokeCliCommand502JSONResponse{
|
|
WorkspaceOperationErrorJSONResponse: workspaceOperationError(protocolErr),
|
|
}
|
|
}
|
|
}
|
|
|
|
func generatedCliCapabilities(capabilities []cliservice.Capability) []tuttigenerated.CliCapability {
|
|
if len(capabilities) == 0 {
|
|
return []tuttigenerated.CliCapability{}
|
|
}
|
|
result := make([]tuttigenerated.CliCapability, 0, len(capabilities))
|
|
for _, capability := range capabilities {
|
|
result = append(result, generatedCliCapability(capability))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func generatedCliCapability(capability cliservice.Capability) tuttigenerated.CliCapability {
|
|
var description *string
|
|
if capability.Description != "" {
|
|
description = stringPointer(capability.Description)
|
|
}
|
|
var inputSchema *map[string]interface{}
|
|
if len(capability.InputSchema) > 0 {
|
|
schema := make(map[string]interface{}, len(capability.InputSchema))
|
|
for key, value := range capability.InputSchema {
|
|
schema[key] = value
|
|
}
|
|
inputSchema = &schema
|
|
}
|
|
return tuttigenerated.CliCapability{
|
|
Id: capability.ID,
|
|
Path: capability.Path,
|
|
Summary: capability.Summary,
|
|
Description: description,
|
|
Visibility: generatedCliCapabilityVisibility(capability.Visibility),
|
|
InputSchema: inputSchema,
|
|
Output: generatedCliCapabilityOutput(capability.Output),
|
|
Execution: generatedCliCommandExecution(capability.Execution),
|
|
HandlerTimeoutMs: intPointerIfPositive(capability.HandlerTimeoutMs),
|
|
Source: generatedCliCapabilitySource(capability.Source),
|
|
}
|
|
}
|
|
|
|
func generatedCliCommandExecution(execution *cliservice.CommandExecution) *tuttigenerated.CliCommandExecution {
|
|
if execution == nil {
|
|
return nil
|
|
}
|
|
mode := tuttigenerated.CliCommandExecutionMode(execution.Mode)
|
|
return &tuttigenerated.CliCommandExecution{Mode: mode}
|
|
}
|
|
|
|
func generatedCliCapabilityVisibility(visibility cliservice.CapabilityVisibility) *tuttigenerated.CliCapabilityVisibility {
|
|
result := tuttigenerated.Public
|
|
if cliservice.NormalizeCapabilityVisibility(visibility) == cliservice.CapabilityVisibilityIntegration {
|
|
result = tuttigenerated.Integration
|
|
}
|
|
return &result
|
|
}
|
|
|
|
func generatedCliCapabilitySource(source cliservice.CapabilitySource) tuttigenerated.CliCapabilitySource {
|
|
if source.Kind == cliservice.CapabilitySourceApp {
|
|
return tuttigenerated.CliCapabilitySource{
|
|
Kind: tuttigenerated.CliCapabilitySourceKindApp,
|
|
AppId: stringPointerIfNotBlank(source.AppID),
|
|
AppName: stringPointerIfNotBlank(source.AppName),
|
|
IconUrl: stringPointerIfNotBlank(source.IconURL),
|
|
CliDescription: stringPointerIfNotBlank(source.CLIDescription),
|
|
AppDescription: stringPointerIfNotBlank(source.AppDescription),
|
|
DocumentationFile: stringPointerIfNotBlank(source.DocumentationFile),
|
|
DocumentationPath: stringPointerIfNotBlank(source.DocumentationPath),
|
|
}
|
|
}
|
|
return tuttigenerated.CliCapabilitySource{Kind: tuttigenerated.CliCapabilitySourceKindBuiltin}
|
|
}
|
|
|
|
func generatedCliCapabilityOutput(output cliservice.CapabilityOutput) tuttigenerated.CliCapabilityOutput {
|
|
return tuttigenerated.CliCapabilityOutput{
|
|
DefaultMode: generatedCliOutputMode(output.DefaultMode),
|
|
Json: output.JSON,
|
|
Table: generatedCliTableOutput(output.Table),
|
|
}
|
|
}
|
|
|
|
func generatedCliTableOutput(output *cliservice.TableOutput) *tuttigenerated.CliTableOutput {
|
|
if output == nil {
|
|
return nil
|
|
}
|
|
return &tuttigenerated.CliTableOutput{Columns: generatedCliTableColumns(output.Columns)}
|
|
}
|
|
|
|
func generatedCliCommandOutput(output cliservice.CommandOutput) *tuttigenerated.CliCommandOutput {
|
|
result := &tuttigenerated.CliCommandOutput{
|
|
Kind: generatedCliOutputMode(output.Kind),
|
|
}
|
|
if len(output.Columns) > 0 {
|
|
columns := generatedCliTableColumns(output.Columns)
|
|
result.Columns = &columns
|
|
}
|
|
if output.Rows != nil {
|
|
rows := make([]map[string]interface{}, 0, len(output.Rows))
|
|
for _, row := range output.Rows {
|
|
converted := make(map[string]interface{}, len(row))
|
|
for key, value := range row {
|
|
converted[key] = value
|
|
}
|
|
rows = append(rows, converted)
|
|
}
|
|
result.Rows = &rows
|
|
}
|
|
if len(output.Value) > 0 {
|
|
value := make(map[string]interface{}, len(output.Value))
|
|
for key, entry := range output.Value {
|
|
value[key] = entry
|
|
}
|
|
result.Value = &value
|
|
}
|
|
if len(output.Warnings) > 0 {
|
|
warnings := make([]tuttigenerated.CliCommandWarning, 0, len(output.Warnings))
|
|
for _, warning := range output.Warnings {
|
|
warnings = append(warnings, tuttigenerated.CliCommandWarning{
|
|
Code: warning.Code,
|
|
Message: warning.Message,
|
|
})
|
|
}
|
|
result.Warnings = &warnings
|
|
}
|
|
if output.Continuation != nil {
|
|
state := tuttigenerated.CliCommandContinuationState(output.Continuation.State)
|
|
result.Continuation = &tuttigenerated.CliCommandContinuation{
|
|
State: state, RetryAfterMs: output.Continuation.RetryAfterMs,
|
|
}
|
|
}
|
|
if output.Text != "" {
|
|
result.Text = stringPointer(output.Text)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func intPointerIfPositive(value int) *int {
|
|
if value <= 0 {
|
|
return nil
|
|
}
|
|
return &value
|
|
}
|
|
|
|
func generatedCliTableColumns(columns []cliservice.TableColumn) []tuttigenerated.CliTableColumn {
|
|
if len(columns) == 0 {
|
|
return []tuttigenerated.CliTableColumn{}
|
|
}
|
|
result := make([]tuttigenerated.CliTableColumn, 0, len(columns))
|
|
for _, column := range columns {
|
|
result = append(result, tuttigenerated.CliTableColumn{
|
|
Key: column.Key,
|
|
Label: column.Label,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func generatedCliOutputMode(mode cliservice.OutputMode) tuttigenerated.CliOutputMode {
|
|
switch mode {
|
|
case cliservice.OutputModeJSON:
|
|
return tuttigenerated.Json
|
|
case cliservice.OutputModePlain:
|
|
return tuttigenerated.Plain
|
|
case cliservice.OutputModeMarkdown:
|
|
return tuttigenerated.Markdown
|
|
default:
|
|
return tuttigenerated.Table
|
|
}
|
|
}
|
|
|
|
func serviceCliOutputMode(mode *tuttigenerated.CliOutputMode) cliservice.OutputMode {
|
|
if mode == nil {
|
|
return ""
|
|
}
|
|
switch *mode {
|
|
case tuttigenerated.Json:
|
|
return cliservice.OutputModeJSON
|
|
case tuttigenerated.Plain:
|
|
return cliservice.OutputModePlain
|
|
case tuttigenerated.Markdown:
|
|
return cliservice.OutputModeMarkdown
|
|
default:
|
|
return cliservice.OutputModeTable
|
|
}
|
|
}
|
|
|
|
func serviceCliContext(contextValue *tuttigenerated.CliInvokeContext) cliservice.InvokeContext {
|
|
if contextValue == nil {
|
|
return cliservice.InvokeContext{Source: "cli"}
|
|
}
|
|
workspaceID := ""
|
|
if contextValue.WorkspaceID != nil {
|
|
workspaceID = *contextValue.WorkspaceID
|
|
}
|
|
parentCommandID := ""
|
|
if contextValue.ParentCommandId != nil {
|
|
parentCommandID = *contextValue.ParentCommandId
|
|
}
|
|
agentSessionID := ""
|
|
if contextValue.AgentSessionId != nil {
|
|
agentSessionID = *contextValue.AgentSessionId
|
|
}
|
|
agentCWD := ""
|
|
if contextValue.AgentCwd != nil {
|
|
agentCWD = *contextValue.AgentCwd
|
|
}
|
|
agentRailPlacementJSON := ""
|
|
if contextValue.AgentRailPlacementJSON != nil {
|
|
agentRailPlacementJSON = *contextValue.AgentRailPlacementJSON
|
|
}
|
|
appID := ""
|
|
if contextValue.AppId != nil {
|
|
appID = *contextValue.AppId
|
|
}
|
|
return cliservice.InvokeContext{
|
|
AppID: appID,
|
|
Source: contextValue.Source,
|
|
WorkspaceID: workspaceID,
|
|
ParentCommandID: parentCommandID,
|
|
AgentSessionID: agentSessionID,
|
|
AgentCWD: agentCWD,
|
|
AgentRailPlacementJSON: agentRailPlacementJSON,
|
|
}
|
|
}
|
|
|
|
func generatedCliInput(input *map[string]interface{}) map[string]any {
|
|
if input == nil {
|
|
return map[string]any{}
|
|
}
|
|
result := make(map[string]any, len(*input))
|
|
for key, value := range *input {
|
|
result[key] = value
|
|
}
|
|
return result
|
|
}
|