diff --git a/examples/workflow-dispatch-agent.yml b/examples/workflow-dispatch-agent.yml new file mode 100644 index 00000000..af651025 --- /dev/null +++ b/examples/workflow-dispatch-agent.yml @@ -0,0 +1,49 @@ +name: Claude Automation Workflow + +on: + workflow_dispatch: + inputs: + task: + description: "Task for Claude to perform" + required: true + type: string + default: "Update dependencies and run tests" + branch: + description: "Branch to work on" + required: false + type: string + default: "main" + +jobs: + claude-automation: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + id-token: write + actions: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.branch || 'main' }} + + - name: Run Claude Code in Agent Mode + uses: anthropics/claude-code-action@beta + with: + mode: agent + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + override_prompt: | + You are running in an automated workflow context. + + Task: ${{ github.event.inputs.task }} + + Current branch: ${{ github.event.inputs.branch || 'main' }} + + Please complete the requested task. Remember: + - You have full access to the repository + - You can make commits and push changes + - Focus on the specific task provided + - Provide clear feedback about what you're doing diff --git a/src/create-prompt/index.ts b/src/create-prompt/index.ts index 27b32816..d75cf4bd 100644 --- a/src/create-prompt/index.ts +++ b/src/create-prompt/index.ts @@ -34,6 +34,16 @@ const BASE_ALLOWED_TOOLS = [ ]; const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"]; +function getEntityNumberXml(eventData: EventData): string { + if (eventData.isPR && "prNumber" in eventData) { + return `${eventData.prNumber}`; + } + if ("issueNumber" in eventData) { + return `${eventData.issueNumber}`; + } + return ""; +} + export function buildAllowedToolsString( customAllowedTools?: string[], includeActionsTools: boolean = false, @@ -125,8 +135,10 @@ export function prepareContext( const isPR = context.isPR; // Get PR/Issue number from entityNumber - const prNumber = isPR ? context.entityNumber.toString() : undefined; - const issueNumber = !isPR ? context.entityNumber.toString() : undefined; + const prNumber = + isPR && context.entityNumber ? context.entityNumber.toString() : undefined; + const issueNumber = + !isPR && context.entityNumber ? context.entityNumber.toString() : undefined; // Extract trigger username and comment data based on event type let triggerUsername: string | undefined; @@ -338,6 +350,24 @@ export function prepareContext( }; break; + case "workflow_dispatch": + eventData = { + eventName: "workflow_dispatch", + isPR: false, + ...(baseBranch && { baseBranch }), + ...(claudeBranch && { claudeBranch }), + }; + break; + + case "schedule": + eventData = { + eventName: "schedule", + isPR: false, + ...(baseBranch && { baseBranch }), + ...(claudeBranch && { claudeBranch }), + }; + break; + default: throw new Error(`Unsupported event type: ${eventName}`); } @@ -400,6 +430,18 @@ export function getEventTypeAndContext(envVars: PreparedContext): { : `pull request event`, }; + case "workflow_dispatch": + return { + eventType: "WORKFLOW_DISPATCH", + triggerContext: `workflow dispatch event`, + }; + + case "schedule": + return { + eventType: "SCHEDULE", + triggerContext: `scheduled automation event`, + }; + default: throw new Error(`Unexpected event type`); } @@ -407,11 +449,12 @@ export function getEventTypeAndContext(envVars: PreparedContext): { function getCommitInstructions( eventData: EventData, - githubData: FetchDataResult, + githubData: FetchDataResult | null, context: PreparedContext, useCommitSigning: boolean, ): string { const coAuthorLine = + githubData && (githubData.triggerDisplayName ?? context.triggerUsername !== "Unknown") ? `Co-authored-by: ${githubData.triggerDisplayName ?? context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>` : ""; @@ -466,8 +509,26 @@ function getCommitInstructions( function substitutePromptVariables( template: string, context: PreparedContext, - githubData: FetchDataResult, + githubData: FetchDataResult | null, ): string { + // Handle automation events without GitHub data + if (!githubData) { + const { eventData } = context; + const variables: Record = { + EVENT_TYPE: eventData.eventName, + REPOSITORY: context.repository, + TRIGGER_USERNAME: context.triggerUsername ?? "automation", + CURRENT_BRANCH: eventData.claudeBranch || eventData.baseBranch || "main", + BASE_BRANCH: eventData.baseBranch || "main", + }; + + return Object.entries(variables).reduce( + (prompt, [key, value]) => + prompt.replace(new RegExp(`{{${key}}}`, "g"), value), + template, + ); + } + const { contextData, comments, reviewData, changedFilesWithSHA } = githubData; const { eventData } = context; @@ -528,7 +589,7 @@ function substitutePromptVariables( export function generatePrompt( context: PreparedContext, - githubData: FetchDataResult, + githubData: FetchDataResult | null, useCommitSigning: boolean, ): string { if (context.overridePrompt) { @@ -539,6 +600,59 @@ export function generatePrompt( ); } + const { eventData } = context; + + // Handle automation events that don't have GitHub data + if (!githubData) { + // For automation events, we have minimal context + const { eventType, triggerContext } = getEventTypeAndContext(context); + + let promptContent = `You are Claude, an AI assistant designed to help with GitHub ${eventData.eventName === "workflow_dispatch" ? "workflow dispatch" : "scheduled automation"} tasks. Think carefully as you analyze the context and respond appropriately. Here's the context for your current task: + + +Repository: ${context.repository} +Event Type: ${eventData.eventName} +Current Branch: ${eventData.claudeBranch || eventData.baseBranch || "main"} +Base Branch: ${eventData.baseBranch || "main"} +Actor: ${context.triggerUsername ?? "automation"} + + +${eventType} +false +${triggerContext} +${context.repository} +${context.triggerUsername ?? "automation"} +${ + context.directPrompt + ? ` +IMPORTANT: The following are direct instructions from the automation workflow: + +${sanitizeContent(context.directPrompt)} +` + : "" +} + + +You have been triggered by an automated workflow. Follow these guidelines: + +1. **Context**: You are running in an automated context without a specific issue or PR. +2. **Branch**: You are currently on the ${eventData.claudeBranch || eventData.baseBranch || "main"} branch. +3. **Tools**: You have access to file system and Git tools to make changes. +${ + useCommitSigning + ? "4. **Commits**: Use the MCP file operations server for signed commits." + : "4. **Commits**: You can commit changes directly using Git." +} +5. **Scope**: Focus on the task described${context.directPrompt ? " in the direct_prompt above" : ""}. + +Please proceed with the automated task. + + +${context.customInstructions || ""}`; + + return promptContent; + } + const { contextData, comments, @@ -546,11 +660,10 @@ export function generatePrompt( reviewData, imageUrlMap, } = githubData; - const { eventData } = context; const { eventType, triggerContext } = getEventTypeAndContext(context); - const formattedContext = formatContext(contextData, eventData.isPR); + const formattedContext = formatContext(contextData, eventData.isPR ?? false); const formattedComments = formatComments(comments, imageUrlMap); const formattedReviewComments = eventData.isPR ? formatReviewComments(reviewData, imageUrlMap) @@ -596,17 +709,13 @@ ${eventData.isPR ? formattedChangedFiles || "No files changed" : ""} ${imagesInfo} ${eventType} -${eventData.isPR ? "true" : "false"} +${(eventData.isPR ?? false) ? "true" : "false"} ${triggerContext} ${context.repository} -${ - eventData.isPR - ? `${eventData.prNumber}` - : `${eventData.issueNumber ?? ""}` -} +${getEntityNumberXml(eventData)} ${context.claudeCommentId} ${context.triggerUsername ?? "Unknown"} -${githubData.triggerDisplayName ?? context.triggerUsername ?? "Unknown"} +${githubData?.triggerDisplayName ?? context.triggerUsername ?? "Unknown"} ${context.triggerPhrase} ${ (eventData.eventName === "issue_comment" || @@ -797,7 +906,7 @@ f. If you are unable to complete certain steps, such as running a linter or test export async function createPrompt( mode: Mode, modeContext: ModeContext, - githubData: FetchDataResult, + githubData: FetchDataResult | null, context: ParsedGitHubContext, ) { try { diff --git a/src/create-prompt/types.ts b/src/create-prompt/types.ts index e7a7130b..25e90064 100644 --- a/src/create-prompt/types.ts +++ b/src/create-prompt/types.ts @@ -88,6 +88,20 @@ type PullRequestEvent = { baseBranch?: string; }; +type WorkflowDispatchEvent = { + eventName: "workflow_dispatch"; + isPR?: false; + baseBranch?: string; + claudeBranch?: string; +}; + +type ScheduleEvent = { + eventName: "schedule"; + isPR?: false; + baseBranch?: string; + claudeBranch?: string; +}; + // Union type for all possible event types export type EventData = | PullRequestReviewCommentEvent @@ -97,7 +111,9 @@ export type EventData = | IssueOpenedEvent | IssueAssignedEvent | IssueLabeledEvent - | PullRequestEvent; + | PullRequestEvent + | WorkflowDispatchEvent + | ScheduleEvent; // Combined type with separate eventData field export type PreparedContext = CommonFields & { diff --git a/src/entrypoints/prepare.ts b/src/entrypoints/prepare.ts index 6653c06a..7b80c6b5 100644 --- a/src/entrypoints/prepare.ts +++ b/src/entrypoints/prepare.ts @@ -7,17 +7,11 @@ import * as core from "@actions/core"; import { setupGitHubToken } from "../github/token"; -import { checkHumanActor } from "../github/validation/actor"; import { checkWritePermissions } from "../github/validation/permissions"; -import { createInitialComment } from "../github/operations/comments/create-initial"; -import { setupBranch } from "../github/operations/branch"; -import { configureGitAuth } from "../github/operations/git-config"; -import { prepareMcpConfig } from "../mcp/install-mcp-server"; import { createOctokit } from "../github/api/client"; -import { fetchGitHubData } from "../github/data/fetcher"; import { parseGitHubContext } from "../github/context"; import { getMode } from "../modes/registry"; -import { createPrompt } from "../create-prompt"; +import { prepare } from "../prepare"; async function run() { try { @@ -51,65 +45,16 @@ async function run() { return; } - // Step 5: Check if actor is human - await checkHumanActor(octokit.rest, context); - - // Step 6: Create initial tracking comment (mode-aware) - // Some modes (e.g., agent mode) may not need tracking comments - let commentId: number | undefined; - let commentData: - | Awaited> - | undefined; - if (mode.shouldCreateTrackingComment()) { - commentData = await createInitialComment(octokit.rest, context); - commentId = commentData.id; - } - - // Step 7: Fetch GitHub data (once for both branch setup and prompt creation) - const githubData = await fetchGitHubData({ - octokits: octokit, - repository: `${context.repository.owner}/${context.repository.repo}`, - prNumber: context.entityNumber.toString(), - isPR: context.isPR, - triggerUsername: context.actor, - }); - - // Step 8: Setup branch - const branchInfo = await setupBranch(octokit, githubData, context); - - // Step 9: Configure git authentication if not using commit signing - if (!context.inputs.useCommitSigning) { - try { - await configureGitAuth(githubToken, context, commentData?.user || null); - } catch (error) { - console.error("Failed to configure git authentication:", error); - throw error; - } - } - - // Step 10: Create prompt file - const modeContext = mode.prepareContext(context, { - commentId, - baseBranch: branchInfo.baseBranch, - claudeBranch: branchInfo.claudeBranch, - }); - - await createPrompt(mode, modeContext, githubData, context); - - // Step 11: Get MCP configuration - const additionalMcpConfig = process.env.MCP_CONFIG || ""; - const mcpConfig = await prepareMcpConfig({ - githubToken, - owner: context.repository.owner, - repo: context.repository.repo, - branch: branchInfo.claudeBranch || branchInfo.currentBranch, - baseBranch: branchInfo.baseBranch, - additionalMcpConfig, - claudeCommentId: commentId?.toString() || "", - allowedTools: context.inputs.allowedTools, + // Step 5: Use the new modular prepare function + const result = await prepare({ context, + octokit, + mode, + githubToken, }); - core.setOutput("mcp_config", mcpConfig); + + // Set the MCP config output + core.setOutput("mcp_config", result.mcpConfig); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(`Prepare step failed with error: ${errorMessage}`); diff --git a/src/entrypoints/update-comment-link.ts b/src/entrypoints/update-comment-link.ts index 85b24552..0e7c87db 100644 --- a/src/entrypoints/update-comment-link.ts +++ b/src/entrypoints/update-comment-link.ts @@ -24,6 +24,13 @@ async function run() { const context = parseGitHubContext(); const { owner, repo } = context.repository; + + // This script is only called for entity-based events + if (!context.entityNumber) { + throw new Error("update-comment-link requires an entityNumber"); + } + const entityNumber = context.entityNumber; + const octokit = createOctokit(githubToken); const serverUrl = GITHUB_SERVER_URL; @@ -73,7 +80,7 @@ async function run() { const { data: pr } = await octokit.rest.pulls.get({ owner, repo, - pull_number: context.entityNumber, + pull_number: entityNumber, }); console.log(`PR state: ${pr.state}`); console.log(`PR comments count: ${pr.comments}`); diff --git a/src/github/context.ts b/src/github/context.ts index 4e0d866e..26e5eead 100644 --- a/src/github/context.ts +++ b/src/github/context.ts @@ -7,6 +7,34 @@ import type { PullRequestReviewEvent, PullRequestReviewCommentEvent, } from "@octokit/webhooks-types"; + +// Custom types for GitHub Actions events that aren't webhooks +export type WorkflowDispatchEvent = { + action?: never; + inputs?: Record; + ref?: string; + repository: { + name: string; + owner: { + login: string; + }; + }; + sender: { + login: string; + }; + workflow: string; +}; + +export type ScheduleEvent = { + action?: never; + schedule?: string; + repository: { + name: string; + owner: { + login: string; + }; + }; +}; import type { ModeName } from "../modes/types"; import { DEFAULT_MODE, isValidMode } from "../modes/registry"; @@ -25,9 +53,11 @@ export type ParsedGitHubContext = { | IssueCommentEvent | PullRequestEvent | PullRequestReviewEvent - | PullRequestReviewCommentEvent; - entityNumber: number; - isPR: boolean; + | PullRequestReviewCommentEvent + | WorkflowDispatchEvent + | ScheduleEvent; + entityNumber?: number; + isPR?: boolean; inputs: { mode: ModeName; triggerPhrase: string; @@ -129,6 +159,20 @@ export function parseGitHubContext(): ParsedGitHubContext { isPR: true, }; } + case "workflow_dispatch": { + return { + ...commonFields, + payload: context.payload as unknown as WorkflowDispatchEvent, + // No entityNumber or isPR for workflow_dispatch + }; + } + case "schedule": { + return { + ...commonFields, + payload: context.payload as unknown as ScheduleEvent, + // No entityNumber or isPR for schedule + }; + } default: throw new Error(`Unsupported event type: ${context.eventName}`); } diff --git a/src/github/operations/comments/create-initial.ts b/src/github/operations/comments/create-initial.ts index 1243035b..8de1fbc5 100644 --- a/src/github/operations/comments/create-initial.ts +++ b/src/github/operations/comments/create-initial.ts @@ -22,6 +22,12 @@ export async function createInitialComment( ) { const { owner, repo } = context.repository; + // Entity events should always have entityNumber + if (!context.entityNumber) { + throw new Error("createInitialComment requires an entityNumber"); + } + const entityNumber = context.entityNumber; + const jobRunLink = createJobRunLink(owner, repo, context.runId); const initialBody = createCommentBody(jobRunLink); @@ -36,7 +42,7 @@ export async function createInitialComment( const comments = await octokit.rest.issues.listComments({ owner, repo, - issue_number: context.entityNumber, + issue_number: entityNumber, }); const existingComment = comments.data.find((comment) => { const idMatch = comment.user?.id === CLAUDE_APP_BOT_ID; @@ -59,7 +65,7 @@ export async function createInitialComment( response = await octokit.rest.issues.createComment({ owner, repo, - issue_number: context.entityNumber, + issue_number: entityNumber, body: initialBody, }); } @@ -68,7 +74,7 @@ export async function createInitialComment( response = await octokit.rest.pulls.createReplyForReviewComment({ owner, repo, - pull_number: context.entityNumber, + pull_number: entityNumber, comment_id: context.payload.comment.id, body: initialBody, }); @@ -77,7 +83,7 @@ export async function createInitialComment( response = await octokit.rest.issues.createComment({ owner, repo, - issue_number: context.entityNumber, + issue_number: entityNumber, body: initialBody, }); } @@ -95,7 +101,7 @@ export async function createInitialComment( const response = await octokit.rest.issues.createComment({ owner, repo, - issue_number: context.entityNumber, + issue_number: entityNumber, body: initialBody, }); diff --git a/src/github/operations/default-branch.ts b/src/github/operations/default-branch.ts new file mode 100644 index 00000000..51e32513 --- /dev/null +++ b/src/github/operations/default-branch.ts @@ -0,0 +1,13 @@ +import type { Octokit } from "@octokit/rest"; + +export async function getDefaultBranch( + rest: Octokit, + owner: string, + repo: string, +): Promise { + const repoResponse = await rest.repos.get({ + owner, + repo, + }); + return repoResponse.data.default_branch; +} diff --git a/src/mcp/install-mcp-server.ts b/src/mcp/install-mcp-server.ts index 31c57dd7..6f3f85c9 100644 --- a/src/mcp/install-mcp-server.ts +++ b/src/mcp/install-mcp-server.ts @@ -141,7 +141,7 @@ export async function prepareMcpConfig( GITHUB_TOKEN: process.env.ACTIONS_TOKEN, REPO_OWNER: owner, REPO_NAME: repo, - PR_NUMBER: context.entityNumber.toString(), + PR_NUMBER: context.entityNumber?.toString() || "", RUNNER_TEMP: process.env.RUNNER_TEMP || "/tmp", }, }; diff --git a/src/prepare/automation-events.ts b/src/prepare/automation-events.ts new file mode 100644 index 00000000..08f6c51f --- /dev/null +++ b/src/prepare/automation-events.ts @@ -0,0 +1,70 @@ +/** + * Prepare logic for automation events (workflow_dispatch, schedule) + * These events don't have associated GitHub entities and require minimal setup + */ + +import * as core from "@actions/core"; +import { prepareMcpConfig } from "../mcp/install-mcp-server"; +import { createPrompt } from "../create-prompt"; +import { getDefaultBranch } from "../github/operations/default-branch"; +import type { PrepareOptions, PrepareResult } from "./types"; + +export async function prepareAutomationEvent({ + context, + octokit, + mode, + githubToken, +}: PrepareOptions): Promise { + // For automation events, we skip: + // - Human actor check (it's automation) + // - Tracking comment (no issue/PR to comment on) + // - GitHub data fetching (no entity to fetch) + // - Branch setup (use default branch or current branch) + + // Get the default branch or use the one specified in inputs + const baseBranch = + context.inputs.baseBranch || + (await getDefaultBranch( + octokit.rest, + context.repository.owner, + context.repository.repo, + )); + + // For automation events, we stay on the current branch (typically main/master) + const branchInfo = { + baseBranch, + currentBranch: baseBranch, + claudeBranch: undefined, + }; + + // Create prompt file with minimal context + const modeContext = mode.prepareContext(context, { + baseBranch: branchInfo.baseBranch, + claudeBranch: branchInfo.claudeBranch, + }); + + // Pass null for githubData since automation events don't have associated entities + await createPrompt(mode, modeContext, null, context); + + // Get MCP configuration + const additionalMcpConfig = process.env.MCP_CONFIG || ""; + const mcpConfig = await prepareMcpConfig({ + githubToken, + owner: context.repository.owner, + repo: context.repository.repo, + branch: branchInfo.currentBranch, + baseBranch: branchInfo.baseBranch, + additionalMcpConfig, + claudeCommentId: "", + allowedTools: context.inputs.allowedTools, + context, + }); + + core.setOutput("mcp_config", mcpConfig); + + return { + commentId: undefined, + branchInfo, + mcpConfig, + }; +} diff --git a/src/prepare/entity-events.ts b/src/prepare/entity-events.ts new file mode 100644 index 00000000..867ae0c5 --- /dev/null +++ b/src/prepare/entity-events.ts @@ -0,0 +1,89 @@ +/** + * Prepare logic for entity-based events (issues, PRs, comments) + * These events have associated GitHub entities that need to be fetched and managed + */ + +import * as core from "@actions/core"; +import { checkHumanActor } from "../github/validation/actor"; +import { createInitialComment } from "../github/operations/comments/create-initial"; +import { setupBranch } from "../github/operations/branch"; +import { configureGitAuth } from "../github/operations/git-config"; +import { prepareMcpConfig } from "../mcp/install-mcp-server"; +import { fetchGitHubData } from "../github/data/fetcher"; +import { createPrompt } from "../create-prompt"; +import type { PrepareOptions, PrepareResult } from "./types"; + +export async function prepareEntityEvent({ + context, + octokit, + mode, + githubToken, +}: PrepareOptions): Promise { + // Check if actor is human + await checkHumanActor(octokit.rest, context); + + // Create initial tracking comment (mode-aware) + let commentId: number | undefined; + let commentData: Awaited> | undefined; + if (mode.shouldCreateTrackingComment()) { + commentData = await createInitialComment(octokit.rest, context); + commentId = commentData.id; + } + + // Fetch GitHub data - entity events always have entityNumber and isPR + if (!context.entityNumber || context.isPR === undefined) { + throw new Error("Entity events must have entityNumber and isPR defined"); + } + + const githubData = await fetchGitHubData({ + octokits: octokit, + repository: `${context.repository.owner}/${context.repository.repo}`, + prNumber: context.entityNumber.toString(), + isPR: context.isPR, + triggerUsername: context.actor, + }); + + // Setup branch + const branchInfo = await setupBranch(octokit, githubData, context); + + // Configure git authentication if not using commit signing + if (!context.inputs.useCommitSigning) { + try { + await configureGitAuth(githubToken, context, commentData?.user || null); + } catch (error) { + console.error("Failed to configure git authentication:", error); + throw error; + } + } + + // Create prompt file + const modeContext = mode.prepareContext(context, { + commentId, + baseBranch: branchInfo.baseBranch, + claudeBranch: branchInfo.claudeBranch, + }); + + await createPrompt(mode, modeContext, githubData, context); + + // Get MCP configuration + const additionalMcpConfig = process.env.MCP_CONFIG || ""; + const mcpConfig = await prepareMcpConfig({ + githubToken, + owner: context.repository.owner, + repo: context.repository.repo, + branch: branchInfo.claudeBranch || branchInfo.currentBranch, + baseBranch: branchInfo.baseBranch, + additionalMcpConfig, + claudeCommentId: commentId?.toString() || "", + allowedTools: context.inputs.allowedTools, + context, + }); + + core.setOutput("mcp_config", mcpConfig); + + return { + commentId, + branchInfo, + mcpConfig, + }; +} diff --git a/src/prepare/index.ts b/src/prepare/index.ts new file mode 100644 index 00000000..72071d04 --- /dev/null +++ b/src/prepare/index.ts @@ -0,0 +1,21 @@ +/** + * Main prepare module that routes to appropriate prepare logic based on event type + */ + +import type { PrepareOptions, PrepareResult } from "./types"; +import { prepareEntityEvent } from "./entity-events"; +import { prepareAutomationEvent } from "./automation-events"; + +const AUTOMATION_EVENTS = ["workflow_dispatch", "schedule"]; + +export async function prepare(options: PrepareOptions): Promise { + const { context } = options; + + if (AUTOMATION_EVENTS.includes(context.eventName)) { + console.log(`Preparing automation event: ${context.eventName}`); + return prepareAutomationEvent(options); + } + + console.log(`Preparing entity-based event: ${context.eventName}`); + return prepareEntityEvent(options); +} diff --git a/src/prepare/types.ts b/src/prepare/types.ts new file mode 100644 index 00000000..5fa8c192 --- /dev/null +++ b/src/prepare/types.ts @@ -0,0 +1,20 @@ +import type { ParsedGitHubContext } from "../github/context"; +import type { Octokits } from "../github/api/client"; +import type { Mode } from "../modes/types"; + +export type PrepareResult = { + commentId?: number; + branchInfo: { + baseBranch: string; + claudeBranch?: string; + currentBranch: string; + }; + mcpConfig: string; +}; + +export type PrepareOptions = { + context: ParsedGitHubContext; + octokit: Octokits; + mode: Mode; + githubToken: string; +};