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
299 lines
12 KiB
Go
299 lines
12 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
tuttigenerated "github.com/tutti-os/tutti/services/tuttid/api/generated"
|
|
"github.com/tutti-os/tutti/services/tuttid/apierrors"
|
|
agentservice "github.com/tutti-os/tutti/services/tuttid/service/agent"
|
|
userprojectservice "github.com/tutti-os/tutti/services/tuttid/service/userproject"
|
|
)
|
|
|
|
func (api DaemonAPI) ScanWorkspaceExternalAgentSessionImports(ctx context.Context, request tuttigenerated.ScanWorkspaceExternalAgentSessionImportsRequestObject) (tuttigenerated.ScanWorkspaceExternalAgentSessionImportsResponseObject, error) {
|
|
if api.AgentSessionService == nil {
|
|
return tuttigenerated.ScanWorkspaceExternalAgentSessionImports503JSONResponse{
|
|
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
|
|
}, nil
|
|
}
|
|
input := agentservice.ExternalImportScanInput{}
|
|
if request.Body != nil {
|
|
input.ArchivePath = optionalStringValue(request.Body.ArchivePath)
|
|
if request.Body.ArchiveKind != nil && !request.Body.ArchiveKind.Valid() {
|
|
return tuttigenerated.ScanWorkspaceExternalAgentSessionImports400JSONResponse{
|
|
InvalidRequestErrorJSONResponse: invalidRequestError(apierrors.InvalidRequest(
|
|
apierrors.ReasonMalformedRequest,
|
|
apierrors.WithDeveloperMessage("unsupported archiveKind"),
|
|
)),
|
|
}, nil
|
|
}
|
|
input.ArchiveKind = externalImportArchiveKindValue(request.Body.ArchiveKind)
|
|
input.Providers = externalImportProvidersFromGenerated(request.Body.Providers)
|
|
if request.Body.Days != nil {
|
|
input.Days = *request.Body.Days
|
|
}
|
|
}
|
|
result, err := api.AgentSessionService.ScanExternalImports(ctx, input)
|
|
if err != nil {
|
|
return writeScanWorkspaceExternalAgentSessionImportsError(err), nil
|
|
}
|
|
return tuttigenerated.ScanWorkspaceExternalAgentSessionImports200JSONResponse(
|
|
generatedExternalImportScanResult(result),
|
|
), nil
|
|
}
|
|
|
|
func (api DaemonAPI) ImportWorkspaceExternalAgentSessions(ctx context.Context, request tuttigenerated.ImportWorkspaceExternalAgentSessionsRequestObject) (tuttigenerated.ImportWorkspaceExternalAgentSessionsResponseObject, error) {
|
|
if api.AgentSessionService == nil {
|
|
return tuttigenerated.ImportWorkspaceExternalAgentSessions503JSONResponse{
|
|
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
|
|
}, nil
|
|
}
|
|
if request.Body == nil || len(request.Body.Projects) == 0 {
|
|
return tuttigenerated.ImportWorkspaceExternalAgentSessions400JSONResponse{
|
|
InvalidRequestErrorJSONResponse: invalidRequestError(apierrors.EmptyBody(apierrors.WithDeveloperMessage("empty project selection"))),
|
|
}, nil
|
|
}
|
|
if request.Body.ArchiveKind != nil && !request.Body.ArchiveKind.Valid() {
|
|
return tuttigenerated.ImportWorkspaceExternalAgentSessions400JSONResponse{
|
|
InvalidRequestErrorJSONResponse: invalidRequestError(apierrors.InvalidRequest(
|
|
apierrors.ReasonMalformedRequest,
|
|
apierrors.WithDeveloperMessage("unsupported archiveKind"),
|
|
)),
|
|
}, nil
|
|
}
|
|
projects := externalImportProjectSelectionsFromGenerated(request.Body.Projects)
|
|
archivePath := optionalStringValue(request.Body.ArchivePath)
|
|
archiveKind := externalImportArchiveKindValue(request.Body.ArchiveKind)
|
|
register := request.Body.RegisterUserProjects == nil || *request.Body.RegisterUserProjects
|
|
importSessions := request.Body.ImportSessions == nil || *request.Body.ImportSessions
|
|
|
|
// Import sessions first, then register only the projects that actually
|
|
// matched at least one valid session so empty projects never get surfaced.
|
|
// validPaths are canonical project paths (see the agent service), so register
|
|
// them directly instead of mapping back to the raw request selections.
|
|
var result agentservice.ExternalImportResult
|
|
var validPaths []string
|
|
if importSessions {
|
|
var err error
|
|
result, err = api.AgentSessionService.ImportExternalSessions(ctx, string(request.WorkspaceID), agentservice.ExternalImportInput{
|
|
ArchivePath: archivePath,
|
|
ArchiveKind: archiveKind,
|
|
Projects: projects,
|
|
})
|
|
if err != nil {
|
|
return writeImportWorkspaceExternalAgentSessionsError(err), nil
|
|
}
|
|
validPaths = result.ProjectPaths
|
|
} else {
|
|
var err error
|
|
validPaths, err = api.AgentSessionService.ExternalImportValidProjectPaths(ctx, agentservice.ExternalImportInput{
|
|
ArchivePath: archivePath,
|
|
ArchiveKind: archiveKind,
|
|
Projects: projects,
|
|
})
|
|
if err != nil {
|
|
return writeImportWorkspaceExternalAgentSessionsError(err), nil
|
|
}
|
|
}
|
|
|
|
registeredSelections, registrationErrors := api.registerExternalImportUserProjects(ctx, externalImportSelectionsFromPaths(validPaths), register)
|
|
result.ImportedProjects = len(registeredSelections)
|
|
result.Errors = append(registrationErrors, result.Errors...)
|
|
return tuttigenerated.ImportWorkspaceExternalAgentSessions200JSONResponse(
|
|
generatedExternalImportResult(result),
|
|
), nil
|
|
}
|
|
|
|
func externalImportSelectionsFromPaths(paths []string) []agentservice.ExternalImportProjectSelection {
|
|
selections := make([]agentservice.ExternalImportProjectSelection, 0, len(paths))
|
|
for _, path := range paths {
|
|
if trimmed := strings.TrimSpace(path); trimmed != "" {
|
|
selections = append(selections, agentservice.ExternalImportProjectSelection{Path: trimmed})
|
|
}
|
|
}
|
|
return selections
|
|
}
|
|
|
|
func externalImportArchiveKindValue(input *tuttigenerated.ExternalAgentImportArchiveKind) string {
|
|
if input == nil {
|
|
return ""
|
|
}
|
|
return string(*input)
|
|
}
|
|
|
|
func externalImportProvidersFromGenerated(input *[]tuttigenerated.WorkspaceAgentProvider) []string {
|
|
if input == nil {
|
|
return nil
|
|
}
|
|
result := make([]string, 0, len(*input))
|
|
for _, provider := range *input {
|
|
result = append(result, string(provider))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func externalImportProjectSelectionsFromGenerated(input []tuttigenerated.ExternalAgentImportProjectSelection) []agentservice.ExternalImportProjectSelection {
|
|
result := make([]agentservice.ExternalImportProjectSelection, 0, len(input))
|
|
for _, project := range input {
|
|
result = append(result, agentservice.ExternalImportProjectSelection{
|
|
Path: project.Path,
|
|
Providers: externalImportProvidersFromGenerated(project.Providers),
|
|
SessionIDs: optionalStringSlice(project.SessionIds),
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func optionalStringSlice(input *[]string) []string {
|
|
if input == nil {
|
|
return nil
|
|
}
|
|
return append([]string(nil), (*input)...)
|
|
}
|
|
|
|
func (api DaemonAPI) registerExternalImportUserProjects(
|
|
ctx context.Context,
|
|
projects []agentservice.ExternalImportProjectSelection,
|
|
register bool,
|
|
) ([]agentservice.ExternalImportProjectSelection, []agentservice.ExternalImportError) {
|
|
if !register || api.UserProjectService == nil {
|
|
return projects, nil
|
|
}
|
|
result := make([]agentservice.ExternalImportProjectSelection, 0, len(projects))
|
|
failures := make([]agentservice.ExternalImportError, 0)
|
|
paths := make([]string, len(projects))
|
|
for index, project := range projects {
|
|
paths[index] = strings.TrimSpace(project.Path)
|
|
}
|
|
registrationErrors := api.UserProjectService.UseMany(ctx, userprojectservice.UseManyInput{Paths: paths})
|
|
for index, project := range projects {
|
|
path := strings.TrimSpace(project.Path)
|
|
if err := registrationErrors[index]; err != nil {
|
|
failure := agentservice.ExternalImportError{
|
|
SourcePath: path,
|
|
Message: err.Error(),
|
|
}
|
|
if path == "" {
|
|
failure.SourcePath = ""
|
|
failure.Message = "project path is empty"
|
|
}
|
|
failures = append(failures, failure)
|
|
continue
|
|
}
|
|
result = append(result, project)
|
|
}
|
|
return result, failures
|
|
}
|
|
|
|
func generatedExternalImportScanResult(result agentservice.ExternalImportScanResult) tuttigenerated.ExternalAgentImportScanResponse {
|
|
return tuttigenerated.ExternalAgentImportScanResponse{
|
|
Errors: generatedExternalImportErrors(result.Errors),
|
|
Projects: generatedExternalImportProjects(result.Projects),
|
|
Providers: generatedExternalImportProviders(result.Providers),
|
|
Sessions: generatedExternalImportSessions(result.Sessions),
|
|
ScannedMessages: result.ScannedMessages,
|
|
ScannedSessions: result.ScannedSessions,
|
|
SkippedSessions: result.SkippedSessions,
|
|
}
|
|
}
|
|
|
|
func generatedExternalImportResult(result agentservice.ExternalImportResult) tuttigenerated.ExternalAgentImportResultResponse {
|
|
return tuttigenerated.ExternalAgentImportResultResponse{
|
|
Errors: generatedExternalImportErrors(result.Errors),
|
|
ImportedMessages: result.ImportedMessages,
|
|
ImportedProjects: result.ImportedProjects,
|
|
ImportedSessions: result.ImportedSessions,
|
|
SkippedSessions: result.SkippedSessions,
|
|
}
|
|
}
|
|
|
|
func generatedExternalImportProviders(providers []agentservice.ExternalImportProvider) []tuttigenerated.ExternalAgentImportProvider {
|
|
result := make([]tuttigenerated.ExternalAgentImportProvider, 0, len(providers))
|
|
for _, provider := range providers {
|
|
generated := tuttigenerated.ExternalAgentImportProvider{
|
|
Available: provider.Available,
|
|
MessageCount: provider.MessageCount,
|
|
Provider: tuttigenerated.WorkspaceAgentProvider(provider.Provider),
|
|
Root: provider.Root,
|
|
SessionCount: provider.SessionCount,
|
|
}
|
|
if errMessage := strings.TrimSpace(provider.Error); errMessage != "" {
|
|
generated.Error = optionalStringPointer(errMessage)
|
|
}
|
|
result = append(result, generated)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func generatedExternalImportProjects(projects []agentservice.ExternalImportProject) []tuttigenerated.ExternalAgentImportProject {
|
|
result := make([]tuttigenerated.ExternalAgentImportProject, 0, len(projects))
|
|
for _, project := range projects {
|
|
generated := tuttigenerated.ExternalAgentImportProject{
|
|
Label: strings.TrimSpace(project.Label),
|
|
MessageCount: project.MessageCount,
|
|
Path: strings.TrimSpace(project.Path),
|
|
Providers: generatedExternalImportProviderNames(project.Providers),
|
|
SessionCount: project.SessionCount,
|
|
}
|
|
if project.LastUpdatedAtUnixMS > 0 {
|
|
generated.LastUpdatedAtUnixMs = &project.LastUpdatedAtUnixMS
|
|
}
|
|
result = append(result, generated)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func generatedExternalImportSessions(sessions []agentservice.ExternalImportSession) []tuttigenerated.ExternalAgentImportSession {
|
|
result := make([]tuttigenerated.ExternalAgentImportSession, 0, len(sessions))
|
|
for _, session := range sessions {
|
|
generated := tuttigenerated.ExternalAgentImportSession{
|
|
Id: strings.TrimSpace(session.ID),
|
|
MessageCount: session.MessageCount,
|
|
ProjectPath: strings.TrimSpace(session.ProjectPath),
|
|
Provider: tuttigenerated.WorkspaceAgentProvider(session.Provider),
|
|
Title: strings.TrimSpace(session.Title),
|
|
}
|
|
// Archive-only imports (e.g. ChatGPT) redact the local path, so only
|
|
// surface sourcePath when present. The contract marks it nullable.
|
|
if sourcePath := strings.TrimSpace(session.SourcePath); sourcePath != "" {
|
|
generated.SourcePath = &sourcePath
|
|
}
|
|
if session.LastUpdatedAtUnixMS > 0 {
|
|
generated.LastUpdatedAtUnixMs = &session.LastUpdatedAtUnixMS
|
|
}
|
|
result = append(result, generated)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func generatedExternalImportProviderNames(providers []string) []tuttigenerated.WorkspaceAgentProvider {
|
|
result := make([]tuttigenerated.WorkspaceAgentProvider, 0, len(providers))
|
|
for _, provider := range providers {
|
|
if strings.TrimSpace(provider) != "" {
|
|
result = append(result, tuttigenerated.WorkspaceAgentProvider(provider))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func generatedExternalImportErrors(errors []agentservice.ExternalImportError) []tuttigenerated.ExternalAgentImportError {
|
|
result := make([]tuttigenerated.ExternalAgentImportError, 0, len(errors))
|
|
for _, item := range errors {
|
|
generated := tuttigenerated.ExternalAgentImportError{
|
|
Message: strings.TrimSpace(item.Message),
|
|
}
|
|
if generated.Message == "" {
|
|
generated.Message = "external import failed"
|
|
}
|
|
if provider := strings.TrimSpace(item.Provider); provider != "" {
|
|
value := tuttigenerated.WorkspaceAgentProvider(provider)
|
|
generated.Provider = &value
|
|
}
|
|
if sourcePath := strings.TrimSpace(item.SourcePath); sourcePath != "" {
|
|
generated.SourcePath = &sourcePath
|
|
}
|
|
result = append(result, generated)
|
|
}
|
|
return result
|
|
}
|