d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
964 lines
26 KiB
TypeScript
964 lines
26 KiB
TypeScript
import { truncate } from '@sim/utils/string'
|
|
import { getCopilotToolDescription } from '@/lib/copilot/tools/descriptions'
|
|
import { isHosted } from '@/lib/core/config/env-flags'
|
|
import { isSubBlockHidden } from '@/lib/workflows/subblocks/visibility'
|
|
import { isCustomBlockType } from '@/blocks/custom/build-config'
|
|
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
|
|
import { DYNAMIC_MODEL_PROVIDERS, PROVIDER_DEFINITIONS } from '@/providers/models'
|
|
import type { ToolConfig } from '@/tools/types'
|
|
|
|
/**
|
|
* Serialize workflow metadata for VFS meta.json.
|
|
*
|
|
* `locked` is the EFFECTIVE lock — true when the workflow is locked directly or
|
|
* sits inside a locked folder. A locked workflow cannot be edited, moved,
|
|
* renamed, or deleted (mutations are rejected server-side with a 423). The
|
|
* mothership should read this before attempting any workflow mutation.
|
|
* `inheritedFolderLock` carries the resolved containing-folder lock (the
|
|
* caller computes folder inheritance; see workspace-vfs materializeWorkflows).
|
|
*/
|
|
export function serializeWorkflowMeta(
|
|
wf: {
|
|
id: string
|
|
name: string
|
|
description?: string | null
|
|
folderId?: string | null
|
|
isDeployed: boolean
|
|
deployedAt?: Date | null
|
|
runCount: number
|
|
lastRunAt?: Date | null
|
|
createdAt: Date
|
|
updatedAt: Date
|
|
locked?: boolean
|
|
},
|
|
options?: { inheritedFolderLock?: boolean }
|
|
): string {
|
|
const directLock = wf.locked ?? false
|
|
const locked = directLock || (options?.inheritedFolderLock ?? false)
|
|
return JSON.stringify(
|
|
{
|
|
id: wf.id,
|
|
name: wf.name,
|
|
description: wf.description || undefined,
|
|
folderId: wf.folderId || undefined,
|
|
locked,
|
|
lockedBy: locked ? (directLock ? 'workflow' : 'folder') : undefined,
|
|
isDeployed: wf.isDeployed,
|
|
deployedAt: wf.deployedAt?.toISOString(),
|
|
runCount: wf.runCount,
|
|
lastRunAt: wf.lastRunAt?.toISOString(),
|
|
createdAt: wf.createdAt.toISOString(),
|
|
updatedAt: wf.updatedAt.toISOString(),
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Serialize execution logs for VFS executions.json.
|
|
* Takes recent execution log rows and produces a summary.
|
|
*/
|
|
export function serializeRecentExecutions(
|
|
executions: Array<{
|
|
id: string
|
|
executionId: string
|
|
status: string
|
|
trigger: string
|
|
startedAt: Date
|
|
endedAt?: Date | null
|
|
totalDurationMs?: number | null
|
|
}>
|
|
): string {
|
|
return JSON.stringify(
|
|
executions.map((e) => ({
|
|
executionId: e.executionId,
|
|
status: e.status,
|
|
trigger: e.trigger,
|
|
startedAt: e.startedAt.toISOString(),
|
|
endedAt: e.endedAt?.toISOString(),
|
|
durationMs: e.totalDurationMs,
|
|
})),
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Serialize knowledge base metadata for VFS meta.json
|
|
*/
|
|
export function serializeKBMeta(kb: {
|
|
id: string
|
|
name: string
|
|
description?: string | null
|
|
embeddingModel: string
|
|
embeddingDimension: number
|
|
tokenCount: number
|
|
createdAt: Date
|
|
updatedAt: Date
|
|
documentCount: number
|
|
connectorTypes?: string[]
|
|
}): string {
|
|
return JSON.stringify(
|
|
{
|
|
id: kb.id,
|
|
name: kb.name,
|
|
description: kb.description || undefined,
|
|
embeddingModel: kb.embeddingModel,
|
|
embeddingDimension: kb.embeddingDimension,
|
|
tokenCount: kb.tokenCount,
|
|
documentCount: kb.documentCount,
|
|
connectorTypes:
|
|
kb.connectorTypes && kb.connectorTypes.length > 0 ? kb.connectorTypes : undefined,
|
|
createdAt: kb.createdAt.toISOString(),
|
|
updatedAt: kb.updatedAt.toISOString(),
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Serialize documents list for VFS documents.json (metadata only, no content)
|
|
*/
|
|
export function serializeDocuments(
|
|
docs: Array<{
|
|
id: string
|
|
filename: string
|
|
fileSize: number
|
|
mimeType: string
|
|
chunkCount: number
|
|
tokenCount: number
|
|
processingStatus: string
|
|
enabled: boolean
|
|
uploadedAt: Date
|
|
}>
|
|
): string {
|
|
return JSON.stringify(
|
|
docs.map((d) => ({
|
|
id: d.id,
|
|
filename: d.filename,
|
|
fileSize: d.fileSize,
|
|
mimeType: d.mimeType,
|
|
chunkCount: d.chunkCount,
|
|
tokenCount: d.tokenCount,
|
|
processingStatus: d.processingStatus,
|
|
enabled: d.enabled,
|
|
uploadedAt: d.uploadedAt.toISOString(),
|
|
})),
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Serialize KB connectors for VFS knowledgebases/{name}/connectors.json.
|
|
* Shows connector type, sync status, and schedule — NOT credentials or source config.
|
|
*/
|
|
export function serializeConnectors(
|
|
connectors: Array<{
|
|
id: string
|
|
connectorType: string
|
|
status: string
|
|
syncMode: string
|
|
syncIntervalMinutes: number
|
|
lastSyncAt: Date | null
|
|
lastSyncError: string | null
|
|
lastSyncDocCount: number | null
|
|
nextSyncAt: Date | null
|
|
consecutiveFailures: number
|
|
createdAt: Date
|
|
}>
|
|
): string {
|
|
return JSON.stringify(
|
|
connectors.map((c) => ({
|
|
id: c.id,
|
|
connectorType: c.connectorType,
|
|
status: c.status,
|
|
syncMode: c.syncMode,
|
|
syncIntervalMinutes: c.syncIntervalMinutes,
|
|
lastSyncAt: c.lastSyncAt?.toISOString(),
|
|
lastSyncError: c.lastSyncError || undefined,
|
|
lastSyncDocCount: c.lastSyncDocCount ?? undefined,
|
|
nextSyncAt: c.nextSyncAt?.toISOString(),
|
|
consecutiveFailures: c.consecutiveFailures,
|
|
createdAt: c.createdAt.toISOString(),
|
|
})),
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Connector config field shape (mirrors ConnectorConfigField from connectors/types.ts
|
|
* but avoids importing React-dependent code into serializers).
|
|
*/
|
|
interface SerializableConfigField {
|
|
id: string
|
|
title: string
|
|
type: string
|
|
placeholder?: string
|
|
required?: boolean
|
|
description?: string
|
|
options?: Array<{ label: string; id: string }>
|
|
}
|
|
|
|
interface SerializableTagDef {
|
|
id: string
|
|
displayName: string
|
|
fieldType: string
|
|
}
|
|
|
|
interface SerializableConnectorConfig {
|
|
id: string
|
|
name: string
|
|
description: string
|
|
version: string
|
|
auth: { mode: string; provider?: string; requiredScopes?: string[] }
|
|
configFields: SerializableConfigField[]
|
|
tagDefinitions?: SerializableTagDef[]
|
|
supportsIncrementalSync?: boolean
|
|
}
|
|
|
|
/**
|
|
* Serialize a single connector type's schema for VFS knowledgebases/connectors/{type}.json.
|
|
* Contains everything the LLM needs to build a valid sourceConfig.
|
|
*/
|
|
export function serializeConnectorSchema(connector: SerializableConnectorConfig): string {
|
|
return JSON.stringify(
|
|
{
|
|
id: connector.id,
|
|
name: connector.name,
|
|
description: connector.description,
|
|
version: connector.version,
|
|
auth: connector.auth,
|
|
configFields: connector.configFields.map((f) => {
|
|
const field: Record<string, unknown> = {
|
|
id: f.id,
|
|
title: f.title,
|
|
type: f.type,
|
|
}
|
|
if (f.required) field.required = true
|
|
if (f.placeholder) field.placeholder = f.placeholder
|
|
if (f.description) field.description = f.description
|
|
if (f.options) field.options = f.options
|
|
return field
|
|
}),
|
|
tagDefinitions: connector.tagDefinitions ?? [],
|
|
supportsIncrementalSync: connector.supportsIncrementalSync ?? false,
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Generate the knowledgebases/connectors/connectors.md overview file.
|
|
* Lists all available connector types with their OAuth providers — enough
|
|
* for the LLM to identify the right type and credential, then read the
|
|
* per-connector schema file for full config details.
|
|
*/
|
|
export function serializeConnectorOverview(connectors: SerializableConnectorConfig[]): string {
|
|
const rows = connectors.map((c) => {
|
|
const provider = c.auth.provider ?? c.auth.mode
|
|
const scopes = c.auth.requiredScopes?.length ? c.auth.requiredScopes.join(', ') : '(none)'
|
|
return `| ${c.id} | ${c.name} | ${provider} | ${scopes} |`
|
|
})
|
|
|
|
return [
|
|
'# Available KB Connectors',
|
|
'',
|
|
'Use `read("knowledgebases/connectors/{type}.json")` to get the full config schema before calling `add_connector`.',
|
|
'',
|
|
'| Type | Name | OAuth Provider | Required Scopes |',
|
|
'|------|------|---------------|-----------------|',
|
|
...rows,
|
|
'',
|
|
'To add a connector, the user must have an OAuth credential for that provider.',
|
|
'Check `environment/credentials.json` for available credential IDs.',
|
|
].join('\n')
|
|
}
|
|
|
|
/**
|
|
* Serialize workspace file metadata for VFS files/{path}/{name}/meta.json.
|
|
*/
|
|
export function serializeFileMeta(file: {
|
|
id: string
|
|
name: string
|
|
folderId?: string | null
|
|
folderPath?: string | null
|
|
vfsPath?: string
|
|
contentType: string
|
|
size: number
|
|
uploadedAt: Date
|
|
}): string {
|
|
return JSON.stringify(
|
|
{
|
|
id: file.id,
|
|
name: file.name,
|
|
folderId: file.folderId || undefined,
|
|
folderPath: file.folderPath || undefined,
|
|
vfsPath: file.vfsPath,
|
|
contentType: file.contentType,
|
|
size: file.size,
|
|
uploadedAt: file.uploadedAt.toISOString(),
|
|
readContentWith: file.vfsPath ? `${file.vfsPath}/content` : undefined,
|
|
note: 'This is file metadata only. To read the file text/bytes, read the readContentWith path (i.e. append /content).',
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Serialize table metadata for VFS tables/{name}/meta.json
|
|
*/
|
|
export function serializeTableMeta(table: {
|
|
id: string
|
|
name: string
|
|
description?: string | null
|
|
schema: unknown
|
|
rowCount: number
|
|
maxRows: number
|
|
createdAt: Date | string
|
|
updatedAt: Date | string
|
|
}): string {
|
|
return JSON.stringify(
|
|
{
|
|
id: table.id,
|
|
name: table.name,
|
|
description: table.description || undefined,
|
|
schema: table.schema,
|
|
rowCount: table.rowCount,
|
|
maxRows: table.maxRows,
|
|
createdAt: table.createdAt instanceof Date ? table.createdAt.toISOString() : table.createdAt,
|
|
updatedAt: table.updatedAt instanceof Date ? table.updatedAt.toISOString() : table.updatedAt,
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Returns the static model list from PROVIDER_DEFINITIONS for VFS serialization.
|
|
* Excludes dynamic providers (ollama, vllm, openrouter) whose models are user-configured.
|
|
* Includes provider ID and whether the model is hosted by Sim (no API key required).
|
|
*/
|
|
interface StaticModelOption {
|
|
id: string
|
|
provider: string
|
|
hosted: boolean
|
|
recommended?: boolean
|
|
speedOptimized?: boolean
|
|
deprecated?: boolean
|
|
}
|
|
|
|
const DYNAMIC_PROVIDERS_NOTE = {
|
|
note: 'The options array above lists Sim\'s static provider catalog. These providers also accept user-configured models that are NOT enumerated here: the user may have additional ids available at runtime (e.g. local Ollama tags). To reference one, prefix the model id with the provider slash below — for example "ollama/llama3.1:8b" instead of the bare "llama3.1:8b". The server rejects bare ids that are not in the catalog; always use the prefix for user-configured models.',
|
|
prefixes: DYNAMIC_MODEL_PROVIDERS.map((p) => `${p}/`),
|
|
} as const
|
|
|
|
function getStaticModelOptionsForVFS(): StaticModelOption[] {
|
|
const hostedProviders = new Set(['openai', 'anthropic', 'google'])
|
|
const dynamicProviders = new Set<string>(DYNAMIC_MODEL_PROVIDERS)
|
|
|
|
const models: StaticModelOption[] = []
|
|
|
|
for (const [providerId, def] of Object.entries(PROVIDER_DEFINITIONS)) {
|
|
if (dynamicProviders.has(providerId)) continue
|
|
for (const model of def.models) {
|
|
const option: StaticModelOption = {
|
|
id: model.id,
|
|
provider: providerId,
|
|
hosted: hostedProviders.has(providerId),
|
|
}
|
|
if (model.recommended) option.recommended = true
|
|
if (model.speedOptimized) option.speedOptimized = true
|
|
if (model.deprecated) option.deprecated = true
|
|
models.push(option)
|
|
}
|
|
}
|
|
|
|
return models
|
|
}
|
|
|
|
/**
|
|
* Serialize a SubBlockConfig for the VFS component schema.
|
|
* Strips functions and UI-only fields. Includes static options arrays.
|
|
*/
|
|
function serializeSubBlock(sb: SubBlockConfig): Record<string, unknown> {
|
|
const result: Record<string, unknown> = {
|
|
id: sb.id,
|
|
type: sb.type,
|
|
}
|
|
if (sb.title) result.title = sb.title
|
|
if (sb.required === true) result.required = true
|
|
if (sb.defaultValue !== undefined) result.defaultValue = sb.defaultValue
|
|
if (sb.mode) result.mode = sb.mode
|
|
if (sb.canonicalParamId) result.canonicalParamId = sb.canonicalParamId
|
|
|
|
// Include static options arrays for dropdowns
|
|
if (Array.isArray(sb.options)) {
|
|
result.options = sb.options
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Serialize a block schema for VFS components/blocks/{type}.json
|
|
*/
|
|
export function serializeBlockSchema(block: BlockConfig): string {
|
|
// Custom blocks bake their `workflowId`/`inputMapping` as `hidden` sub-blocks;
|
|
// treat `hidden` as hidden for them so those never reach the agent's schema.
|
|
const customBlock = isCustomBlockType(block.type)
|
|
const hiddenIds = new Set(
|
|
block.subBlocks
|
|
.filter((sb) => isSubBlockHidden(sb) || (customBlock && sb.hidden))
|
|
.map((sb) => sb.id)
|
|
)
|
|
|
|
const subBlocks = block.subBlocks
|
|
.filter((sb) => !hiddenIds.has(sb.id))
|
|
.map((sb) => {
|
|
const serialized = serializeSubBlock(sb)
|
|
|
|
if (sb.id === 'model' && sb.type === 'combobox' && typeof sb.options === 'function') {
|
|
serialized.options = getStaticModelOptionsForVFS()
|
|
serialized.dynamicProviders = DYNAMIC_PROVIDERS_NOTE
|
|
}
|
|
|
|
return serialized
|
|
})
|
|
|
|
const inputs =
|
|
block.inputs && hiddenIds.size > 0
|
|
? Object.fromEntries(Object.entries(block.inputs).filter(([key]) => !hiddenIds.has(key)))
|
|
: block.inputs
|
|
|
|
return JSON.stringify(
|
|
{
|
|
type: block.type,
|
|
name: block.name,
|
|
description: block.description,
|
|
category: block.category,
|
|
longDescription: block.longDescription || undefined,
|
|
bestPractices: block.bestPractices || undefined,
|
|
triggerAllowed: block.triggerAllowed || undefined,
|
|
singleInstance: block.singleInstance || undefined,
|
|
// Custom (deploy-as-block) blocks execute via a baked `workflow_executor`
|
|
// internally; that's implementation plumbing, not something the agent
|
|
// configures. Hiding it keeps the block self-contained (fields in, outputs
|
|
// out) so the agent doesn't treat it like the generic workflow block and
|
|
// ask for a workflowId/inputMapping.
|
|
tools: isCustomBlockType(block.type) ? [] : block.tools.access,
|
|
subBlocks,
|
|
inputs,
|
|
outputs: Object.fromEntries(
|
|
Object.entries(block.outputs)
|
|
.filter(([key, val]) => key !== 'visualization' && val != null)
|
|
.map(([key, val]) => [
|
|
key,
|
|
typeof val === 'string'
|
|
? { type: val }
|
|
: { type: val.type, description: (val as { description?: string }).description },
|
|
])
|
|
),
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Serialize OAuth credentials for VFS environment/credentials.json.
|
|
* Shows which integrations are connected — IDs, roles, and scopes, NOT tokens.
|
|
*/
|
|
export function serializeCredentials(
|
|
accounts: Array<{
|
|
id?: string
|
|
providerId: string
|
|
displayName?: string | null
|
|
role?: string | null
|
|
scope: string | null
|
|
createdAt: Date
|
|
}>
|
|
): string {
|
|
return JSON.stringify(
|
|
accounts.map((a) => ({
|
|
id: a.id || undefined,
|
|
provider: a.providerId,
|
|
displayName: a.displayName || undefined,
|
|
role: a.role || undefined,
|
|
scope: a.scope || undefined,
|
|
connectedAt: a.createdAt.toISOString(),
|
|
})),
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Serialize API keys for VFS environment/api-keys.json.
|
|
* Shows key names and types — NOT the actual key values.
|
|
*/
|
|
export function serializeApiKeys(
|
|
keys: Array<{
|
|
id: string
|
|
name: string
|
|
type: string
|
|
lastUsed: Date | null
|
|
createdAt: Date
|
|
expiresAt: Date | null
|
|
}>
|
|
): string {
|
|
return JSON.stringify(
|
|
keys.map((k) => ({
|
|
id: k.id,
|
|
name: k.name,
|
|
type: k.type,
|
|
lastUsed: k.lastUsed?.toISOString(),
|
|
createdAt: k.createdAt.toISOString(),
|
|
expiresAt: k.expiresAt?.toISOString(),
|
|
})),
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Serialize environment variables for VFS environment/variables.json.
|
|
* Shows variable NAMES only — NOT values.
|
|
*/
|
|
export function serializeEnvironmentVariables(
|
|
personalVarNames: string[],
|
|
workspaceVarNames: string[]
|
|
): string {
|
|
return JSON.stringify(
|
|
{
|
|
personal: personalVarNames,
|
|
workspace: workspaceVarNames,
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/** Input types for deployment serialization. */
|
|
export interface DeploymentData {
|
|
workflowId: string
|
|
isDeployed: boolean
|
|
deployedAt?: Date | null
|
|
needsRedeployment?: boolean
|
|
api?: {
|
|
version: number
|
|
createdAt: Date
|
|
} | null
|
|
chat?: {
|
|
id: string
|
|
identifier: string
|
|
title: string
|
|
description?: string | null
|
|
authType: string
|
|
customizations: unknown
|
|
isActive: boolean
|
|
} | null
|
|
mcp: Array<{
|
|
serverId: string
|
|
serverName: string
|
|
toolId: string
|
|
toolName: string
|
|
toolDescription?: string | null
|
|
}>
|
|
versions?: Array<{
|
|
id: string
|
|
version: number
|
|
name: string | null
|
|
description: string | null
|
|
isActive: boolean
|
|
createdAt: Date
|
|
}>
|
|
}
|
|
|
|
/**
|
|
* Serialize all deployment configurations for VFS deployment.json.
|
|
* Only includes keys for active deployment types.
|
|
*/
|
|
export function serializeDeployments(data: DeploymentData): string {
|
|
const result: Record<string, unknown> = {}
|
|
|
|
if (data.needsRedeployment !== undefined) {
|
|
result.needsRedeployment = data.needsRedeployment
|
|
}
|
|
|
|
if (data.isDeployed) {
|
|
result.api = {
|
|
isDeployed: true,
|
|
deployedAt: data.deployedAt?.toISOString(),
|
|
apiEndpoint: `/api/workflows/${data.workflowId}/execute`,
|
|
...(data.api ? { version: data.api.version } : {}),
|
|
}
|
|
}
|
|
|
|
if (data.chat) {
|
|
result.chat = {
|
|
id: data.chat.id,
|
|
identifier: data.chat.identifier,
|
|
chatUrl: `/chat/${data.chat.identifier}`,
|
|
title: data.chat.title,
|
|
description: data.chat.description || undefined,
|
|
authType: data.chat.authType,
|
|
customizations: data.chat.customizations,
|
|
isActive: data.chat.isActive,
|
|
}
|
|
}
|
|
|
|
if (data.mcp.length > 0) {
|
|
result.mcp = data.mcp.map((m) => ({
|
|
serverId: m.serverId,
|
|
serverName: m.serverName,
|
|
toolId: m.toolId,
|
|
toolName: m.toolName,
|
|
toolDescription: m.toolDescription || undefined,
|
|
}))
|
|
}
|
|
|
|
return JSON.stringify(result, null, 2)
|
|
}
|
|
|
|
/**
|
|
* Serialize deployment version history for VFS workflows/{name}/versions.json.
|
|
* Lists all versions without full state — use the diff_workflows tool to compare a version,
|
|
* or load_deployment to restore one into the draft.
|
|
*/
|
|
export function serializeVersions(
|
|
versions: Array<{
|
|
id: string
|
|
version: number
|
|
name: string | null
|
|
description: string | null
|
|
isActive: boolean
|
|
createdAt: Date
|
|
}>
|
|
): string {
|
|
return JSON.stringify(
|
|
versions.map((v) => ({
|
|
id: v.id,
|
|
version: v.version,
|
|
name: v.name || undefined,
|
|
description: v.description || undefined,
|
|
isActive: v.isActive,
|
|
createdAt: v.createdAt.toISOString(),
|
|
})),
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Serialize a custom tool for VFS custom-tools/{name}.json
|
|
*/
|
|
export function serializeCustomTool(tool: {
|
|
id: string
|
|
title: string
|
|
schema: unknown
|
|
code: string
|
|
}): string {
|
|
return JSON.stringify(
|
|
{
|
|
id: tool.id,
|
|
title: tool.title,
|
|
schema: tool.schema,
|
|
codePreview: truncate(tool.code, 500),
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Serialize an MCP server for VFS agent/mcp-servers/{name}.json
|
|
*/
|
|
export function serializeMcpServer(server: {
|
|
id: string
|
|
name: string
|
|
url: string | null
|
|
transport: string | null
|
|
enabled: boolean
|
|
connectionStatus: string | null
|
|
}): string {
|
|
return JSON.stringify(
|
|
{
|
|
id: server.id,
|
|
name: server.name,
|
|
url: server.url,
|
|
transport: server.transport,
|
|
enabled: server.enabled,
|
|
connectionStatus: server.connectionStatus,
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Serialize a skill for VFS agent/skills/{name}.json
|
|
*/
|
|
export function serializeSkill(s: {
|
|
id: string
|
|
name: string
|
|
description: string
|
|
content: string
|
|
createdAt: Date
|
|
}): string {
|
|
return JSON.stringify(
|
|
{
|
|
id: s.id,
|
|
name: s.name,
|
|
description: s.description,
|
|
contentPreview: truncate(s.content, 500),
|
|
createdAt: s.createdAt.toISOString(),
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Serialize an integration/tool schema for VFS components/integrations/{service}/{operation}.json
|
|
*/
|
|
export function serializeIntegrationSchema(tool: ToolConfig): string {
|
|
const hostedApiKeyParam = isHosted && tool.hosting ? tool.hosting.apiKeyParam : null
|
|
|
|
return JSON.stringify(
|
|
{
|
|
// The full registry id is the agent-callable id (deferred tools are sent
|
|
// with this exact id; no stripping). Surface it verbatim so "copy the id
|
|
// field and load it" matches the callable tool and the block's tools.access.
|
|
id: tool.id,
|
|
name: tool.name,
|
|
description: getCopilotToolDescription(tool, { isHosted }),
|
|
version: tool.version,
|
|
oauth: tool.oauth
|
|
? { required: tool.oauth.required, provider: tool.oauth.provider }
|
|
: undefined,
|
|
params: tool.params
|
|
? {
|
|
...Object.fromEntries(
|
|
Object.entries(tool.params)
|
|
.filter(([key, val]) => val != null && key !== hostedApiKeyParam)
|
|
.map(([key, val]) => [
|
|
key,
|
|
{
|
|
type: val.type,
|
|
required: val.required,
|
|
description: val.description,
|
|
default: val.default,
|
|
},
|
|
])
|
|
),
|
|
...(tool.oauth?.required && {
|
|
credentialId: {
|
|
type: 'string',
|
|
required: false,
|
|
description:
|
|
'Credential ID to use for this OAuth tool call. For Copilot/Superagent execution, pass this explicitly. Get valid IDs from environment/credentials.json.',
|
|
},
|
|
}),
|
|
}
|
|
: undefined,
|
|
outputs: tool.outputs
|
|
? Object.fromEntries(
|
|
Object.entries(tool.outputs)
|
|
.filter(([, val]) => val != null)
|
|
.map(([key, val]) => [key, { type: val.type, description: val.description }])
|
|
)
|
|
: undefined,
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Serialize a trigger schema for VFS components/triggers/{provider}/{id}.json
|
|
*/
|
|
export function serializeTriggerSchema(trigger: {
|
|
id: string
|
|
name: string
|
|
provider: string
|
|
description: string
|
|
version: string
|
|
subBlocks: SubBlockConfig[]
|
|
outputs: Record<string, unknown>
|
|
webhook?: { method?: string; headers?: Record<string, string> }
|
|
}): string {
|
|
return JSON.stringify(
|
|
{
|
|
id: trigger.id,
|
|
name: trigger.name,
|
|
provider: trigger.provider,
|
|
description: trigger.description,
|
|
version: trigger.version,
|
|
webhook: trigger.webhook || undefined,
|
|
subBlocks: trigger.subBlocks.map(serializeSubBlock),
|
|
outputs: trigger.outputs,
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Serialize a built-in trigger block for VFS components/triggers/sim/{type}.json
|
|
*/
|
|
export function serializeBuiltinTriggerSchema(block: BlockConfig): string {
|
|
return JSON.stringify(
|
|
{
|
|
type: block.type,
|
|
name: block.name,
|
|
description: block.description,
|
|
longDescription: block.longDescription || undefined,
|
|
category: 'builtin',
|
|
triggers: block.triggers || undefined,
|
|
subBlocks: block.subBlocks.map(serializeSubBlock),
|
|
inputs: block.inputs,
|
|
outputs: block.outputs,
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
interface TriggerOverviewEntry {
|
|
id: string
|
|
name: string
|
|
provider: string
|
|
description: string
|
|
}
|
|
|
|
/**
|
|
* Serialize a triggers.md overview for VFS components/triggers/triggers.md
|
|
*/
|
|
export function serializeTriggerOverview(
|
|
builtinTriggers: TriggerOverviewEntry[],
|
|
externalTriggers: TriggerOverviewEntry[]
|
|
): string {
|
|
const lines: string[] = ['# Triggers', '']
|
|
|
|
lines.push('## Built-in Triggers', '')
|
|
lines.push('| ID | Name | Description |')
|
|
lines.push('|----|------|-------------|')
|
|
for (const t of builtinTriggers) {
|
|
lines.push(`| ${t.id} | ${t.name} | ${t.description} |`)
|
|
}
|
|
|
|
lines.push('')
|
|
lines.push('## External Triggers', '')
|
|
lines.push('| Provider | ID | Name | Description |')
|
|
lines.push('|----------|----|------|-------------|')
|
|
for (const t of externalTriggers) {
|
|
lines.push(`| ${t.provider} | ${t.id} | ${t.name} | ${t.description} |`)
|
|
}
|
|
|
|
lines.push('')
|
|
return lines.join('\n')
|
|
}
|
|
|
|
/**
|
|
* Serialize job metadata for VFS jobs/{id}/meta.json
|
|
*/
|
|
export function serializeJobMeta(job: {
|
|
id: string
|
|
title: string | null
|
|
prompt: string
|
|
cronExpression: string | null
|
|
timezone: string | null
|
|
status: string
|
|
lifecycle: string
|
|
successCondition: string | null
|
|
maxRuns: number | null
|
|
runCount: number
|
|
nextRunAt: Date | null
|
|
lastRanAt: Date | null
|
|
sourceTaskName: string | null
|
|
sourceChatId: string | null
|
|
createdAt: Date
|
|
}): string {
|
|
return JSON.stringify(
|
|
{
|
|
id: job.id,
|
|
title: job.title || undefined,
|
|
prompt: job.prompt,
|
|
cronExpression: job.cronExpression || undefined,
|
|
timezone: job.timezone || 'UTC',
|
|
status: job.status,
|
|
lifecycle: job.lifecycle,
|
|
successCondition: job.successCondition || undefined,
|
|
maxRuns: job.maxRuns ?? undefined,
|
|
runCount: job.runCount,
|
|
nextRunAt: job.nextRunAt?.toISOString(),
|
|
lastRanAt: job.lastRanAt?.toISOString(),
|
|
sourceTaskName: job.sourceTaskName || undefined,
|
|
sourceChatId: job.sourceChatId || undefined,
|
|
createdAt: job.createdAt.toISOString(),
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
|
|
export function serializeTaskSession(task: {
|
|
id: string
|
|
title: string
|
|
messageCount: number
|
|
createdAt: Date
|
|
updatedAt: Date
|
|
}): string {
|
|
return [
|
|
`# ${task.title}`,
|
|
'',
|
|
`- **Chat ID:** ${task.id}`,
|
|
`- **Created:** ${task.createdAt.toISOString()}`,
|
|
`- **Updated:** ${task.updatedAt.toISOString()}`,
|
|
`- **Messages:** ${task.messageCount}`,
|
|
'',
|
|
].join('\n')
|
|
}
|
|
|
|
export function serializeTaskChat(rawMessages: unknown[]): string {
|
|
const filtered: { role: string; content: string }[] = []
|
|
|
|
for (const msg of rawMessages) {
|
|
if (!msg || typeof msg !== 'object') continue
|
|
const m = msg as Record<string, unknown>
|
|
const role = m.role as string | undefined
|
|
if (role !== 'user' && role !== 'assistant') continue
|
|
|
|
let content = ''
|
|
if (role === 'assistant' && Array.isArray(m.contentBlocks)) {
|
|
const textParts: string[] = []
|
|
for (const block of m.contentBlocks) {
|
|
if (
|
|
block &&
|
|
typeof block === 'object' &&
|
|
(block as any).type === 'text' &&
|
|
(block as any).content
|
|
) {
|
|
textParts.push((block as any).content)
|
|
}
|
|
}
|
|
content = textParts.join('')
|
|
}
|
|
|
|
if (!content && typeof m.content === 'string') {
|
|
content = m.content
|
|
}
|
|
|
|
if (!content) continue
|
|
filtered.push({ role, content })
|
|
}
|
|
|
|
return JSON.stringify(filtered, null, 2)
|
|
}
|