UNPKG

@ubiquity-os/plugin-sdk

Version:

SDK for plugin support.

1,282 lines (1,256 loc) 46.4 kB
// src/actions.ts import * as core from "@actions/core"; import { Value as Value3 } from "@sinclair/typebox/value"; import { Logs } from "@ubiquity-os/ubiquity-os-logger"; // src/error.ts import { LogReturn } from "@ubiquity-os/ubiquity-os-logger"; function getErrorStatus(err) { if (!err || typeof err !== "object") return null; const candidate = err; const directStatus = candidate.status ?? candidate.response?.status; if (typeof directStatus === "number" && Number.isFinite(directStatus)) return directStatus; if (typeof directStatus === "string" && directStatus.trim()) { const parsed = Number.parseInt(directStatus, 10); if (Number.isFinite(parsed)) return parsed; } if (err instanceof Error) { const match = /LLM API error:\s*(\d{3})/i.exec(err.message); if (match) { const parsed = Number.parseInt(match[1], 10); if (Number.isFinite(parsed)) return parsed; } } return null; } function logByStatus(context, message, metadata) { const status = getErrorStatus(metadata.err); const payload = { ...metadata, ...status ? { status } : {} }; if (status && status >= 500) return context.logger.error(message, payload); if (status && status >= 400) return context.logger.warn(message, payload); if (status && status >= 300) return context.logger.debug(message, payload); if (status && status >= 200) return context.logger.ok(message, payload); if (status && status >= 100) return context.logger.info(message, payload); return context.logger.error(message, payload); } function transformError(context, error) { if (error instanceof LogReturn) { return error; } if (error instanceof AggregateError) { const message = error.errors.map((err) => { if (err instanceof LogReturn) { return err.logMessage.raw; } if (err instanceof Error) { return err.message; } return String(err); }).join("\n\n"); return logByStatus(context, message, { err: error }); } if (error instanceof Error) { return logByStatus(context, error.message, { err: error }); } return logByStatus(context, String(error), { err: error }); } // src/helpers/runtime-info.ts import { getRuntimeKey } from "hono/adapter"; // src/helpers/github-context.ts import * as github from "@actions/github"; function getGithubContext() { const override = globalThis.__UOS_GITHUB_CONTEXT__; if (override) { return override; } const module = github; const context = module.context ?? module.default?.context; if (!context) { throw new Error("GitHub context is unavailable."); } return context; } // src/helpers/runtime-info.ts function formatUtcTimestamp(timestamp) { return new Date(timestamp).toISOString().replace(/\.\d{3}Z$/, "Z"); } var PluginRuntimeInfo = class _PluginRuntimeInfo { static _instance = null; _env = {}; constructor(env) { if (env) { this._env = env; } } static getInstance(env) { if (!_PluginRuntimeInfo._instance) { switch (getRuntimeKey()) { case "workerd": _PluginRuntimeInfo._instance = new CfRuntimeInfo(env); break; case "deno": if (process.env.CI) { _PluginRuntimeInfo._instance = new NodeRuntimeInfo(env); } else { _PluginRuntimeInfo._instance = new DenoRuntimeInfo(env); } break; case "node": _PluginRuntimeInfo._instance = new NodeRuntimeInfo(env); break; default: _PluginRuntimeInfo._instance = new NodeRuntimeInfo(env); break; } } return _PluginRuntimeInfo._instance; } }; var CfRuntimeInfo = class extends PluginRuntimeInfo { get version() { return this._env.CLOUDFLARE_VERSION_METADATA?.id ?? "CLOUDFLARE_VERSION_METADATA"; } get runUrl() { const accountId = this._env.CLOUDFLARE_ACCOUNT_ID ?? "<missing-cloudflare-account-id>"; const workerName = this._env.CLOUDFLARE_WORKER_NAME; const toTime = Date.now() + 6e4; const fromTime = Date.now() - 6e4; const timeParam = encodeURIComponent(`{"type":"absolute","to":${toTime},"from":${fromTime}}`); return `https://dash.cloudflare.com/${accountId}/workers/services/view/${workerName}/production/observability/logs?granularity=0&time=${timeParam}`; } }; var NodeRuntimeInfo = class extends PluginRuntimeInfo { get version() { return getGithubContext().sha; } get runUrl() { const context = getGithubContext(); return context.payload.repository ? `${context.payload.repository?.html_url}/actions/runs/${context.runId}` : "http://localhost"; } }; var DenoRuntimeInfo = class extends PluginRuntimeInfo { get version() { return Deno.env.get("DENO_DEPLOYMENT_ID"); } get runUrl() { const orgSlug = Deno.env.get("DENO_DEPLOY_ORG_SLUG") || "<missing-deno-org-slug>"; const appSlug = Deno.env.get("DENO_DEPLOY_APP_SLUG") || "<missing-deno-app-slug>"; const baseUrl = `https://console.deno.com/${orgSlug}/${appSlug}/observability/logs`; const searchParams = new URLSearchParams(); searchParams.set("start", formatUtcTimestamp(Date.now() - 6e4)); searchParams.set("end", formatUtcTimestamp(Date.now() + 6e4)); searchParams.set("tz", "Etc/UTC"); return `${baseUrl}?${searchParams.toString()}`; } }; // src/util.ts import { LOG_LEVEL } from "@ubiquity-os/ubiquity-os-logger"; // src/constants.ts var KERNEL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs96DOU+JqM8SyNXOB6u3 uBKIFiyrcST/LZTYN6y7LeJlyCuGPqSDrWCfjU9Ph5PLf9TWiNmeM8DGaOpwEFC7 U3NRxOSglo4plnQ5zRwIHHXvxyK400sQP2oISXymISuBQWjEIqkC9DybQrKwNzf+ I0JHWPqmwMIw26UvVOtXGOOWBqTkk+N2+/9f8eDIJP5QQVwwszc8s1rXOsLMlVIf wShw7GO4E2jyK8TSJKpyjV8eb1JJMDwFhPiRrtZfQJUtDf2mV/67shQww61BH2Y/ Plnalo58kWIbkqZoq1yJrL5sFb73osM5+vADTXVn79bkvea7W19nSkdMiarYt4Hq JQIDAQAB -----END PUBLIC KEY----- `; // src/util.ts function sanitizeMetadata(obj) { return JSON.stringify(obj, null, 2).replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/--/g, "&#45;&#45;"); } function sanitizeLlmResponse(input) { const trimmed = input.trim(); if (!trimmed) { return trimmed; } if (trimmed.startsWith("```")) { let result = trimmed.replace(/^```[a-z0-9+-]*\s*(?:\r\n|\n)?/i, ""); if (result.endsWith("```")) { result = result.slice(0, -3); result = result.replace(/[\r\n]+$/, ""); } return result.trim(); } if (trimmed.startsWith("`") && trimmed.endsWith("`")) { return trimmed.slice(1, -1).trim(); } return trimmed; } function getPluginOptions(options) { return { // Important to use || and not ?? to not consider empty strings kernelPublicKey: options?.kernelPublicKey || KERNEL_PUBLIC_KEY, logLevel: options?.logLevel || LOG_LEVEL.INFO, postCommentOnError: options?.postCommentOnError ?? true, settingsSchema: options?.settingsSchema, envSchema: options?.envSchema, commandSchema: options?.commandSchema, // eslint-disable-next-line sonarjs/deprecation bypassSignatureVerification: options?.bypassSignatureVerification || false, returnDataToKernel: options?.returnDataToKernel ?? true }; } // src/comment.ts var COMMAND_RESPONSE_KIND = "command-response"; var COMMAND_RESPONSE_MARKER = `"commentKind": "${COMMAND_RESPONSE_KIND}"`; var COMMAND_RESPONSE_COMMENT_LIMIT = 50; var RECENT_COMMENTS_QUERY = ( /* GraphQL */ ` query ($owner: String!, $repo: String!, $number: Int!, $last: Int!) { repository(owner: $owner, name: $repo) { issueOrPullRequest(number: $number) { __typename ... on Issue { comments(last: $last) { nodes { id body isMinimized minimizedReason author { login } } } } ... on PullRequest { comments(last: $last) { nodes { id body isMinimized minimizedReason author { login } } } } } } } ` ); var MINIMIZE_COMMENT_MUTATION = ` mutation($id: ID!, $classifier: ReportedContentClassifiers!) { minimizeComment(input: { subjectId: $id, classifier: $classifier }) { minimizedComment { isMinimized minimizedReason } } } `; function logByStatus2(logger, message, status, metadata) { const payload = { ...metadata, ...status ? { status } : {} }; if (status && status >= 500) return logger.error(message, payload); if (status && status >= 400) return logger.warn(message, payload); if (status && status >= 300) return logger.debug(message, payload); if (status && status >= 200) return logger.ok(message, payload); if (status && status >= 100) return logger.info(message, payload); return logger.error(message, payload); } var CommentHandler = class _CommentHandler { static HEADER_NAME = "UbiquityOS"; _lastCommentId = { reviewCommentId: null, issueCommentId: null }; _commandResponsePolicyApplied = false; async _updateIssueComment(context, params) { if (!this._lastCommentId.issueCommentId) { throw context.logger.error("issueCommentId is missing"); } const commentData = await context.octokit.rest.issues.updateComment({ owner: params.owner, repo: params.repo, comment_id: this._lastCommentId.issueCommentId, body: params.body }); return { ...commentData.data, issueNumber: params.issueNumber }; } async _updateReviewComment(context, params) { if (!this._lastCommentId.reviewCommentId) { throw context.logger.error("reviewCommentId is missing"); } const commentData = await context.octokit.rest.pulls.updateReviewComment({ owner: params.owner, repo: params.repo, comment_id: this._lastCommentId.reviewCommentId, body: params.body }); return { ...commentData.data, issueNumber: params.issueNumber }; } async _createNewComment(context, params) { if (params.commentId) { const commentData2 = await context.octokit.rest.pulls.createReplyForReviewComment({ owner: params.owner, repo: params.repo, pull_number: params.issueNumber, comment_id: params.commentId, body: params.body }); this._lastCommentId.reviewCommentId = commentData2.data.id; return { ...commentData2.data, issueNumber: params.issueNumber }; } const commentData = await context.octokit.rest.issues.createComment({ owner: params.owner, repo: params.repo, issue_number: params.issueNumber, body: params.body }); this._lastCommentId.issueCommentId = commentData.data.id; return { ...commentData.data, issueNumber: params.issueNumber }; } _getIssueNumber(context) { if ("issue" in context.payload) return context.payload.issue.number; if ("pull_request" in context.payload) return context.payload.pull_request.number; if ("discussion" in context.payload) return context.payload.discussion.number; return void 0; } _getCommentId(context) { return "pull_request" in context.payload && "comment" in context.payload ? context.payload.comment.id : void 0; } _getCommentNodeId(context) { const payload = context.payload; const nodeId = payload.comment?.node_id; return typeof nodeId === "string" && nodeId.trim() ? nodeId : null; } _extractIssueContext(context) { if (!("repository" in context.payload) || !context.payload.repository?.owner?.login) { return null; } const issueNumber = this._getIssueNumber(context); if (!issueNumber) return null; return { issueNumber, commentId: this._getCommentId(context), owner: context.payload.repository.owner.login, repo: context.payload.repository.name }; } _extractIssueLocator(context) { if (!("issue" in context.payload) && !("pull_request" in context.payload)) { return null; } const issueContext = this._extractIssueContext(context); if (!issueContext) return null; return { owner: issueContext.owner, repo: issueContext.repo, issueNumber: issueContext.issueNumber }; } _shouldApplyCommandResponsePolicy(context) { return Boolean(context.command); } _isCommandResponseComment(body) { return typeof body === "string" && body.includes(COMMAND_RESPONSE_MARKER); } _getGraphqlClient(context) { const graphql = context.octokit.graphql; return typeof graphql === "function" ? graphql : null; } async _fetchRecentComments(context, locator, last = COMMAND_RESPONSE_COMMENT_LIMIT) { const graphql = this._getGraphqlClient(context); if (!graphql) return []; try { const data = await graphql(RECENT_COMMENTS_QUERY, { owner: locator.owner, repo: locator.repo, number: locator.issueNumber, last }); const nodes = data.repository?.issueOrPullRequest?.comments?.nodes ?? []; return nodes.filter((node) => Boolean(node)); } catch (error) { context.logger.debug("Failed to fetch recent comments (non-fatal)", { err: error }); return []; } } _findPreviousCommandResponseComment(comments, currentCommentId) { for (let idx = comments.length - 1; idx >= 0; idx -= 1) { const comment = comments[idx]; if (!comment) continue; if (currentCommentId && comment.id === currentCommentId) continue; if (this._isCommandResponseComment(comment.body)) { return comment; } } return null; } async _minimizeComment(context, commentNodeId, classifier = "RESOLVED") { const graphql = this._getGraphqlClient(context); if (!graphql) return; try { await graphql(MINIMIZE_COMMENT_MUTATION, { id: commentNodeId, classifier }); } catch (error) { context.logger.debug("Failed to minimize comment (non-fatal)", { err: error, commentNodeId }); } } async _applyCommandResponsePolicy(context) { if (this._commandResponsePolicyApplied) return; this._commandResponsePolicyApplied = true; if (!this._shouldApplyCommandResponsePolicy(context)) return; const locator = this._extractIssueLocator(context); const commentNodeId = this._getCommentNodeId(context); const comments = locator ? await this._fetchRecentComments(context, locator) : []; const current = commentNodeId ? comments.find((comment) => comment.id === commentNodeId) : null; const isCurrentMinimized = current?.isMinimized ?? false; if (commentNodeId && !isCurrentMinimized) { await this._minimizeComment(context, commentNodeId); } const previous = this._findPreviousCommandResponseComment(comments, commentNodeId); if (previous && !previous.isMinimized) { await this._minimizeComment(context, previous.id); } } _processMessage(context, message) { if (message instanceof Error) { const metadata2 = { message: message.message, name: message.name, stack: message.stack }; const status = getErrorStatus(message); const logReturn = logByStatus2(context.logger, message.message, status, metadata2); return { metadata: { ...metadata2, ...status ? { status } : {} }, logMessage: logReturn.logMessage }; } const stackLine = message.metadata?.error?.stack?.split("\n")[2]; const callerMatch = stackLine ? /at (\S+)/.exec(stackLine) : null; const metadata = message.metadata ? { ...message.metadata, message: message.metadata.message, stack: message.metadata.stack || message.metadata.error?.stack, caller: message.metadata.caller || callerMatch?.[1] } : { ...message }; return { metadata, logMessage: message.logMessage }; } _getInstigatorName(context) { if ("installation" in context.payload && context.payload.installation && "account" in context.payload.installation && context.payload.installation?.account?.name) { return context.payload.installation?.account?.name; } return context.payload.sender?.login || _CommentHandler.HEADER_NAME; } _createMetadataContent(context, metadata) { const jsonPretty = sanitizeMetadata(metadata); const instigatorName = this._getInstigatorName(context); const runUrl = PluginRuntimeInfo.getInstance().runUrl; const version = PluginRuntimeInfo.getInstance().version; const callingFnName = metadata.caller || "anonymous"; return { header: `<!-- ${_CommentHandler.HEADER_NAME} - ${callingFnName} - ${version} - @${instigatorName} - ${runUrl}`, jsonPretty }; } _formatMetadataContent(logMessage, header, jsonPretty) { const metadataVisible = ["```json", jsonPretty, "```"].join("\n"); const metadataHidden = [header, jsonPretty, "-->"].join("\n"); return logMessage?.type === "fatal" ? [metadataVisible, metadataHidden].join("\n") : metadataHidden; } /* * Creates the body for the comment, embeds the metadata and the header hidden in the body as well. */ createCommentBody(context, message, options) { return this._createCommentBody(context, message, options); } _createCommentBody(context, message, options) { const { metadata, logMessage } = this._processMessage(context, message); const shouldTagCommandResponse = options?.commentKind && typeof metadata === "object" && !("commentKind" in metadata); const metadataWithKind = shouldTagCommandResponse ? { ...metadata, commentKind: options?.commentKind } : metadata; const { header, jsonPretty } = this._createMetadataContent(context, metadataWithKind); const metadataContent = this._formatMetadataContent(logMessage, header, jsonPretty); return `${options?.raw ? logMessage?.raw : logMessage?.diff} ${metadataContent} `; } async postComment(context, message, options = { updateComment: true, raw: false }) { await this._applyCommandResponsePolicy(context); const issueContext = this._extractIssueContext(context); if (!issueContext) { context.logger.warn("Cannot post comment: missing issue context in payload"); return null; } const shouldTagCommandResponse = this._shouldApplyCommandResponsePolicy(context); const body = this._createCommentBody(context, message, { ...options, commentKind: options.commentKind ?? (shouldTagCommandResponse ? COMMAND_RESPONSE_KIND : void 0) }); const { issueNumber, commentId, owner, repo } = issueContext; const params = { owner, repo, body, issueNumber }; if (options.updateComment) { if (this._lastCommentId.issueCommentId && !("pull_request" in context.payload && "comment" in context.payload)) { return this._updateIssueComment(context, params); } if (this._lastCommentId.reviewCommentId && "pull_request" in context.payload && "comment" in context.payload) { return this._updateReviewComment(context, params); } } return this._createNewComment(context, { ...params, commentId }); } }; // src/helpers/command.ts import { Value } from "@sinclair/typebox/value"; function getCommand(inputs, pluginOptions) { let command = null; if (inputs.command && pluginOptions.commandSchema) { try { command = Value.Decode(pluginOptions.commandSchema, Value.Default(pluginOptions.commandSchema, inputs.command)); } catch (e) { console.dir(...Value.Errors(pluginOptions.commandSchema, inputs.command), { depth: null }); throw e; } } else if (inputs.command) { command = inputs.command; } return command; } // src/helpers/compression.ts import { brotliCompressSync, brotliDecompressSync } from "node:zlib"; function compressString(str) { const input = Buffer.from(str, "utf8"); const compressed = brotliCompressSync(input); return Buffer.from(compressed).toString("base64"); } function decompressString(compressed) { const buffer = Buffer.from(compressed, "base64"); const decompressed = brotliDecompressSync(buffer); return Buffer.from(decompressed).toString("utf8"); } // src/octokit.ts import { Octokit } from "@octokit/core"; import { paginateGraphQL } from "@octokit/plugin-paginate-graphql"; import { paginateRest } from "@octokit/plugin-paginate-rest"; import { restEndpointMethods } from "@octokit/plugin-rest-endpoint-methods"; import { retry } from "@octokit/plugin-retry"; import { throttling } from "@octokit/plugin-throttling"; var defaultOptions = { throttle: { onAbuseLimit: (retryAfter, options, octokit) => { octokit.log.warn(`Abuse limit hit with "${options.method} ${options.url}", retrying in ${retryAfter} seconds.`); return true; }, onRateLimit: (retryAfter, options, octokit) => { octokit.log.warn(`Rate limit hit with "${options.method} ${options.url}", retrying in ${retryAfter} seconds.`); return true; }, onSecondaryRateLimit: (retryAfter, options, octokit) => { octokit.log.warn(`Secondary rate limit hit with "${options.method} ${options.url}", retrying in ${retryAfter} seconds.`); return true; } } }; var customOctokit = Octokit.plugin(throttling, retry, paginateRest, restEndpointMethods, paginateGraphQL).defaults((instanceOptions) => { return { ...defaultOptions, ...instanceOptions }; }); // src/signature.ts async function verifySignature(publicKeyPem, inputs, signature) { try { const inputsOrdered = { stateId: inputs.stateId, eventName: inputs.eventName, eventPayload: inputs.eventPayload, settings: inputs.settings, authToken: inputs.authToken, ubiquityKernelToken: inputs.ubiquityKernelToken, ref: inputs.ref, command: inputs.command }; const pemContents = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replace(/\s+/g, ""); const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0)); const publicKey = await crypto.subtle.importKey( "spki", binaryDer, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, true, ["verify"] ); const signatureArray = Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)); const dataArray = new TextEncoder().encode(JSON.stringify(inputsOrdered)); return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", publicKey, signatureArray, dataArray); } catch (error) { console.error(error); return false; } } // src/types/input-schema.ts import { Type as T2 } from "@sinclair/typebox"; // src/types/command.ts import { Type as T } from "@sinclair/typebox"; var commandCallSchema = T.Union([T.Null(), T.Object({ name: T.String(), parameters: T.Unknown() })]); // src/types/util.ts import { Type } from "@sinclair/typebox"; import { Value as Value2 } from "@sinclair/typebox/value"; function jsonType(type, decompress = false) { return Type.Transform(Type.String()).Decode((value) => { const parsed = JSON.parse(decompress ? decompressString(value) : value); return Value2.Decode(type, Value2.Default(type, parsed)); }).Encode((value) => JSON.stringify(value)); } // src/types/input-schema.ts var inputSchema = T2.Object({ stateId: T2.String(), eventName: T2.String(), eventPayload: jsonType(T2.Record(T2.String(), T2.Any()), true), command: jsonType(commandCallSchema), authToken: T2.String(), ubiquityKernelToken: T2.Optional(T2.String()), settings: jsonType(T2.Record(T2.String(), T2.Any())), ref: T2.String(), signature: T2.String() }); // src/actions.ts async function handleError(context, pluginOptions, error) { console.error(error); const loggerError = transformError(context, error); core.setFailed(loggerError.logMessage.diff); if (pluginOptions.postCommentOnError && loggerError) { await context.commentHandler.postComment(context, loggerError); } } function getDispatchTokenOrFail(pluginOptions) { if (!pluginOptions.returnDataToKernel) return null; const token = process.env.PLUGIN_GITHUB_TOKEN; if (!token) { core.setFailed("Error: PLUGIN_GITHUB_TOKEN env is not set"); return null; } return token; } async function getInputsOrFail(pluginOptions) { const githubContext = getGithubContext(); const body = githubContext.payload.inputs; const inputSchemaErrors = [...Value3.Errors(inputSchema, body)]; if (inputSchemaErrors.length) { console.dir(inputSchemaErrors, { depth: null }); core.setFailed(`Error: Invalid inputs payload: ${inputSchemaErrors.map((o) => o.message).join(", ")}`); return null; } if (!pluginOptions.bypassSignatureVerification) { const signature = typeof body.signature === "string" ? body.signature : ""; if (!signature) { core.setFailed("Error: Missing signature"); return null; } const isValid = await verifySignature(pluginOptions.kernelPublicKey, body, signature); if (!isValid) { core.setFailed("Error: Invalid signature"); return null; } } return Value3.Decode(inputSchema, body); } function decodeWithSchema(schema, value, errorMessage) { if (!schema) { return { value }; } try { return { value: Value3.Decode(schema, Value3.Default(schema, value)) }; } catch (error) { console.dir(...Value3.Errors(schema, value), { depth: null }); const err = new Error(errorMessage); err.cause = error; return { value: null, error: err }; } } async function createActionsPlugin(handler, options) { const pluginOptions = getPluginOptions(options); const pluginGithubToken = getDispatchTokenOrFail(pluginOptions); if (pluginOptions.returnDataToKernel && !pluginGithubToken) { return; } const inputs = await getInputsOrFail(pluginOptions); if (!inputs) { return; } const context = { eventName: inputs.eventName, payload: inputs.eventPayload, command: null, authToken: inputs.authToken, ubiquityKernelToken: inputs.ubiquityKernelToken, octokit: new customOctokit({ auth: inputs.authToken }), config: inputs.settings, env: process.env, logger: new Logs(pluginOptions.logLevel), commentHandler: new CommentHandler() }; const configResult = decodeWithSchema(pluginOptions.settingsSchema, inputs.settings, "Error: Invalid settings provided."); if (!configResult.value) { await handleError(context, pluginOptions, configResult.error ?? new Error("Error: Invalid settings provided.")); return; } context.config = configResult.value; const envResult = decodeWithSchema(pluginOptions.envSchema, process.env, "Error: Invalid environment provided."); if (!envResult.value) { await handleError(context, pluginOptions, envResult.error ?? new Error("Error: Invalid environment provided.")); return; } context.env = envResult.value; try { context.command = getCommand(inputs, pluginOptions); const result = await handler(context); core.setOutput("result", result); if (pluginOptions.returnDataToKernel && pluginGithubToken) { await returnDataToKernel(pluginGithubToken, inputs.stateId, result); } } catch (error) { await handleError(context, pluginOptions, error); } } async function returnDataToKernel(repoToken, stateId, output) { const githubContext = getGithubContext(); const octokit = new customOctokit({ auth: repoToken }); await octokit.rest.repos.createDispatchEvent({ owner: githubContext.repo.owner, repo: githubContext.repo.repo, event_type: "return-data-to-ubiquity-os-kernel", client_payload: { state_id: stateId, output: output ? compressString(JSON.stringify(output)) : null } }); } // src/markdown.ts var VOID_TAGS = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]); function cleanMarkdown(md, options = {}) { const codeBlockRegex = /(```[\s\S]*?```|~~~[\s\S]*?~~~)/g; const { tags = [], shouldCollapseEmptyLines = false } = options; const segments = []; let lastIndex = 0; const matches = [...md.matchAll(codeBlockRegex)]; for (const match of matches) { if (match.index > lastIndex) { segments.push(processSegment(md.slice(lastIndex, match.index), tags, shouldCollapseEmptyLines)); } segments.push(match[0]); lastIndex = match.index + match[0].length; } if (lastIndex < md.length) { segments.push(processSegment(md.slice(lastIndex), tags, shouldCollapseEmptyLines)); } return segments.join(""); } function processSegment(segment, extraTags, shouldCollapseEmptyLines) { const inlineCodeRegex = /`[^`]*`/g; const inlineCodes = []; let s = segment.replace(inlineCodeRegex, (m) => { inlineCodes.push(m); return `__INLINE_CODE_${inlineCodes.length - 1}__`; }); s = s.replace(/<!--[\s\S]*?-->/g, ""); for (const raw of extraTags) { if (!raw) continue; const tag = raw.toLowerCase().trim().replace(/[^\w:-]/g, ""); if (!tag) continue; if (VOID_TAGS.has(tag)) { const voidRe = new RegExp(`<${tag}\\b[^>]*\\/?>`, "gi"); s = s.replace(voidRe, ""); continue; } const pairRe = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, "gi"); let prev; do { prev = s; s = s.replace(pairRe, ""); } while (s !== prev); const openCloseRe = new RegExp(`<\\/?${tag}\\b[^>]*>`, "gi"); s = s.replace(openCloseRe, ""); } s = s.replace(/__INLINE_CODE_(\d+)__/g, (str, idx) => inlineCodes[+idx]); if (shouldCollapseEmptyLines) { s = s.replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n"); } return s; } // src/server.ts import { Value as Value4 } from "@sinclair/typebox/value"; import { Logs as Logs2 } from "@ubiquity-os/ubiquity-os-logger"; import { Hono } from "hono"; import { env as honoEnv } from "hono/adapter"; import { HTTPException } from "hono/http-exception"; // src/types/manifest.ts import { Type as T3 } from "@sinclair/typebox"; import { emitterEventNames } from "@octokit/webhooks"; // src/helpers/runtime-manifest.ts var EMPTY_VALUE = String(); var GITHUB_HEADS_PREFIX = "refs/heads/"; var GITHUB_TAGS_PREFIX = "refs/tags/"; function readRuntimeEnvValue(env, key) { const explicitValue = env?.[key]; if (typeof explicitValue === "string" && explicitValue.trim()) { return explicitValue.trim(); } const runtime = globalThis; if (typeof runtime.Deno?.env?.get === "function") { const denoValue = runtime.Deno.env.get(key); if (typeof denoValue === "string" && denoValue.trim()) { return denoValue.trim(); } } if (typeof process !== "undefined") { const processValue = process.env[key]; if (typeof processValue === "string" && processValue.trim()) { return processValue.trim(); } } return EMPTY_VALUE; } function parseRefNameFromGitHubRef(ref) { if (!ref) { return EMPTY_VALUE; } if (ref.startsWith(GITHUB_HEADS_PREFIX)) { return ref.slice(GITHUB_HEADS_PREFIX.length); } if (ref.startsWith(GITHUB_TAGS_PREFIX)) { return ref.slice(GITHUB_TAGS_PREFIX.length); } return EMPTY_VALUE; } function overrideShortName(shortName, refName) { if (!shortName || !refName) { return shortName; } const separatorIndex = shortName.lastIndexOf("@"); const repository = separatorIndex === -1 ? shortName : shortName.slice(0, separatorIndex); if (!repository) { return shortName; } return `${repository}@${refName}`; } function resolveRuntimeRefName(env) { const explicitRefName = readRuntimeEnvValue(env, "REF_NAME"); if (explicitRefName) { return explicitRefName; } const legacyManifestRefName = readRuntimeEnvValue(env, "PLUGIN_MANIFEST_REF_NAME"); if (legacyManifestRefName) { return legacyManifestRefName; } const githubRefName = readRuntimeEnvValue(env, "GITHUB_REF_NAME"); if (githubRefName) { return githubRefName; } return parseRefNameFromGitHubRef(readRuntimeEnvValue(env, "GITHUB_REF")); } function resolveRuntimeManifest(manifest, env) { const refName = resolveRuntimeRefName(env); if (!refName) { return manifest; } return { ...manifest, short_name: overrideShortName(manifest.short_name, refName) }; } // src/types/manifest.ts var runEvent = T3.Union(emitterEventNames.map((o) => T3.Literal(o))); var exampleCommandExecutionSchema = T3.Object({ commandInvocation: T3.String({ minLength: 1 }), githubContext: T3.Optional(T3.Record(T3.String(), T3.Any())), expectedToolCallResult: T3.Object({ function: T3.String({ minLength: 1 }), parameters: T3.Record(T3.String(), T3.Any()) }) }); var commandSchema = T3.Object({ description: T3.String({ minLength: 1 }), "ubiquity:example": T3.String({ minLength: 1 }), parameters: T3.Optional(T3.Record(T3.String(), T3.Any())), examples: T3.Optional(T3.Array(exampleCommandExecutionSchema, { default: [] })) }); var manifestSchema = T3.Object({ name: T3.String({ minLength: 1 }), short_name: T3.String({ minLength: 1 }), description: T3.Optional(T3.String({ default: "" })), commands: T3.Optional(T3.Record(T3.String({ pattern: "^[A-Za-z-_]+$" }), commandSchema, { default: {} })), "ubiquity:listeners": T3.Optional(T3.Array(runEvent, { default: [] })), configuration: T3.Optional(T3.Record(T3.String(), T3.Any(), { default: {} })), skipBotEvents: T3.Optional(T3.Boolean({ default: true })), homepage_url: T3.Optional(T3.String()) }); // src/server.ts async function handleError2(context, pluginOptions, error) { console.error(error); const loggerError = transformError(context, error); if (pluginOptions.postCommentOnError && loggerError) { await context.commentHandler.postComment(context, loggerError); } throw new HTTPException(500, { message: "Unexpected error" }); } function createPlugin(handler, manifest, options) { const pluginOptions = getPluginOptions(options); const app = new Hono(); app.get("/manifest.json", (ctx) => { return ctx.json(resolveRuntimeManifest(manifest, honoEnv(ctx))); }); app.post("/", async function appPost(ctx) { if (ctx.req.header("content-type") !== "application/json") { throw new HTTPException(400, { message: "Content-Type must be application/json" }); } const body = await ctx.req.json(); const inputSchemaErrors = [...Value4.Errors(inputSchema, body)]; if (inputSchemaErrors.length) { console.dir(inputSchemaErrors, { depth: null }); throw new HTTPException(400, { message: "Invalid body" }); } const signature = body.signature; if (!pluginOptions.bypassSignatureVerification && !await verifySignature(pluginOptions.kernelPublicKey, body, signature)) { throw new HTTPException(400, { message: "Invalid signature" }); } const inputs = Value4.Decode(inputSchema, body); let config; if (pluginOptions.settingsSchema) { try { config = Value4.Decode(pluginOptions.settingsSchema, Value4.Default(pluginOptions.settingsSchema, inputs.settings)); } catch (e) { console.dir(...Value4.Errors(pluginOptions.settingsSchema, inputs.settings), { depth: null }); throw e; } } else { config = inputs.settings; } let env; const honoEnvironment = honoEnv(ctx); if (pluginOptions.envSchema) { try { env = Value4.Decode(pluginOptions.envSchema, Value4.Default(pluginOptions.envSchema, honoEnvironment)); } catch (e) { console.dir(...Value4.Errors(pluginOptions.envSchema, honoEnvironment), { depth: null }); throw e; } } else { env = ctx.env; } const workerName = new URL(inputs.ref).hostname.split(".")[0]; PluginRuntimeInfo.getInstance({ ...env, CLOUDFLARE_WORKER_NAME: workerName }); const command = getCommand(inputs, pluginOptions); const context = { eventName: inputs.eventName, payload: inputs.eventPayload, command, authToken: inputs.authToken, ubiquityKernelToken: inputs.ubiquityKernelToken, octokit: new customOctokit({ auth: inputs.authToken }), config, env, logger: new Logs2(pluginOptions.logLevel), commentHandler: new CommentHandler() }; try { const result = await handler(context); return ctx.json({ stateId: inputs.stateId, output: result ?? {} }); } catch (error) { await handleError2(context, pluginOptions, error); } }); return app; } // src/llm/index.ts var EMPTY_STRING = ""; function normalizeBaseUrl(baseUrl) { let normalized = baseUrl.trim(); while (normalized.endsWith("/")) { normalized = normalized.slice(0, -1); } return normalized; } var MAX_LLM_RETRIES = 2; var RETRY_BACKOFF_MS = [250, 750]; function getRetryDelayMs(attempt) { return RETRY_BACKOFF_MS[Math.min(attempt, RETRY_BACKOFF_MS.length - 1)] ?? 750; } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function getEnvString(name) { if (typeof process === "undefined" || !process?.env) return EMPTY_STRING; return String(process.env[name] ?? EMPTY_STRING).trim(); } function normalizeToken(value) { return typeof value === "string" ? value.trim() : EMPTY_STRING; } function isGitHubToken(token) { return token.trim().startsWith("gh"); } function readKernelRefreshUrl(value) { if (!value || typeof value !== "object") return EMPTY_STRING; return normalizeToken(value.kernelRefreshUrl); } function getKernelRefreshUrl(input) { const direct = readKernelRefreshUrl(input); if (direct) return direct; const fromConfig = readKernelRefreshUrl(input.config); if (fromConfig) return fromConfig; return readKernelRefreshUrl(input.settings); } function updateInputTokens(input, tokens) { if ("authToken" in input) { input.authToken = tokens.authToken; } if ("ubiquityKernelToken" in input) { input.ubiquityKernelToken = tokens.kernelToken; } } function getEnvTokenFromInput(input) { if ("env" in input) { const envValue = input.env; if (envValue && typeof envValue === "object") { const token = normalizeToken(envValue.UOS_AI_TOKEN); if (token) return token; } } return getEnvString("UOS_AI_TOKEN"); } function resolveAuthToken(input, aiAuthToken) { const explicit = normalizeToken(aiAuthToken); if (explicit) return { token: explicit, isGitHub: isGitHubToken(explicit) }; const envToken = getEnvTokenFromInput(input); if (envToken) return { token: envToken, isGitHub: isGitHubToken(envToken) }; const fallback = normalizeToken(input.authToken); if (!fallback) { const err = new Error("Missing auth token; set UOS_AI_TOKEN, pass aiAuthToken, or provide authToken in input"); err.status = 401; throw err; } return { token: fallback, isGitHub: isGitHubToken(fallback) }; } function getAiBaseUrl(options) { if (typeof options.baseUrl === "string" && options.baseUrl.trim()) { return normalizeBaseUrl(options.baseUrl); } const envBaseUrl = getEnvString("UOS_AI_URL"); if (envBaseUrl) return normalizeBaseUrl(envBaseUrl); return "https://ai-ubq-fi.deno.dev"; } async function callLlm(options, input) { const { baseUrl, model, stream: isStream, messages, aiAuthToken, ...rest } = options; ensureMessages(messages); const { token: resolvedAuthToken, isGitHub } = resolveAuthToken(input, aiAuthToken); let authToken = resolvedAuthToken; let kernelToken = "ubiquityKernelToken" in input ? input.ubiquityKernelToken : void 0; const payload = getPayload(input); const { owner, repo, installationId } = getRepoMetadata(payload); const kernelRefreshUrl = getKernelRefreshUrl(input); const hasExplicitAiAuth = Boolean(normalizeToken(aiAuthToken)); if (isGitHub && kernelRefreshUrl && kernelToken && !hasExplicitAiAuth) { const refreshed = await refreshKernelTokens({ url: kernelRefreshUrl, authToken, kernelToken, owner, repo, installationId }); authToken = refreshed.authToken; kernelToken = refreshed.kernelToken; updateInputTokens(input, refreshed); } const url = buildAiUrl(options, baseUrl); const body = JSON.stringify({ ...rest, ...model ? { model } : {}, messages, stream: isStream ?? false }); const headers = buildHeaders(authToken, { owner, repo, installationId, ubiquityKernelToken: isGitHub ? kernelToken : void 0 }); const response = await fetchWithRetry(url, { method: "POST", headers, body }, MAX_LLM_RETRIES); if (isStream) { if (!response.body) { throw new Error("LLM API error: missing response body for streaming request"); } return parseSseStream(response.body); } const rawText = await response.text(); try { return JSON.parse(rawText); } catch (err) { const preview = rawText ? rawText.slice(0, 1e3) : EMPTY_STRING; const message = "LLM API error: failed to parse JSON response from server" + (preview ? `; response body (truncated): ${preview}` : EMPTY_STRING); const error = new Error(message); error.cause = err; error.status = response.status; throw error; } } function ensureMessages(messages) { if (!Array.isArray(messages) || messages.length === 0) { const err = new Error("messages must be a non-empty array"); err.status = 400; throw err; } } function buildAiUrl(options, baseUrl) { return `${getAiBaseUrl({ ...options, baseUrl })}/v1/chat/completions`; } async function refreshKernelTokens(params) { const headers = buildHeaders(params.authToken, { owner: params.owner, repo: params.repo, installationId: params.installationId, ubiquityKernelToken: params.kernelToken }); const response = await fetch(params.url, { method: "POST", headers }); const text = await response.text().catch(() => EMPTY_STRING); if (!response.ok) { const error = new Error(`Kernel refresh error: ${response.status} - ${text}`); error.status = response.status; throw error; } let payload = {}; if (text.trim()) { try { payload = JSON.parse(text); } catch (err) { const details = err instanceof Error ? ` (${err.message})` : ""; const error = new Error(`Kernel refresh error: failed to parse JSON response${details}`); error.status = response.status; throw error; } } const authToken = normalizeToken(payload.authToken); const kernelToken = normalizeToken(payload.ubiquityKernelToken); if (!authToken || !kernelToken) { const error = new Error("Kernel refresh error: response missing authToken or ubiquityKernelToken"); error.status = 502; throw error; } return { authToken, kernelToken, expiresAt: typeof payload.expiresAt === "string" ? payload.expiresAt : null }; } async function fetchWithRetry(url, options, maxRetries) { let attempt = 0; let lastError; while (attempt <= maxRetries) { try { const response = await fetch(url, options); if (response.ok) return response; throw await buildResponseError(response); } catch (error) { lastError = error; if (!shouldRetryError(error, attempt, maxRetries)) throw error; await sleep(getRetryDelayMs(attempt)); attempt += 1; } } throw lastError ?? new Error("LLM API error: request failed after retries"); } async function buildResponseError(response) { const errText = await response.text(); const error = new Error(`LLM API error: ${response.status} - ${errText}`); error.status = response.status; return error; } function shouldRetryError(error, attempt, maxRetries) { if (attempt >= maxRetries) return false; const status = getErrorStatus2(error); if (typeof status === "number") { return status >= 500; } return true; } function getErrorStatus2(error) { return typeof error?.status === "number" ? error.status : void 0; } function getPayload(input) { if ("payload" in input) { return input.payload; } return input.eventPayload; } function getRepoMetadata(payload) { const repoPayload = payload; return { owner: repoPayload?.repository?.owner?.login ?? EMPTY_STRING, repo: repoPayload?.repository?.name ?? EMPTY_STRING, installationId: repoPayload?.installation?.id }; } function buildHeaders(authToken, options) { const headers = { Authorization: `Bearer ${authToken}`, "Content-Type": "application/json" }; if (options.owner) headers["X-GitHub-Owner"] = options.owner; if (options.repo) headers["X-GitHub-Repo"] = options.repo; if (typeof options.installationId === "number" && Number.isFinite(options.installationId)) { headers["X-GitHub-Installation-Id"] = String(options.installationId); } if (options.ubiquityKernelToken) { headers["X-Ubiquity-Kernel-Token"] = options.ubiquityKernelToken; } return headers; } async function* parseSseStream(body) { const reader = body.getReader(); const decoder = new TextDecoder(); let buffer = EMPTY_STRING; try { while (true) { const { value, done: isDone } = await reader.read(); if (isDone) break; buffer += decoder.decode(value, { stream: true }); const { events, remainder } = splitSseEvents(buffer); buffer = remainder; for (const event of events) { const data = getEventData(event); if (!data) continue; if (data.trim() === "[DONE]") return; yield parseEventData(data); } } } finally { reader.releaseLock(); } } function splitSseEvents(buffer) { const normalized = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); const parts = normalized.split("\n\n"); const remainder = parts.pop() ?? EMPTY_STRING; return { events: parts, remainder }; } function getEventData(event) { if (!event.trim()) return null; const dataLines = event.split("\n").filter((line) => line.startsWith("data:")); if (!dataLines.length) return null; const data = dataLines.map((line) => line.startsWith("data: ") ? line.slice(6) : line.slice(5).replace(/^ /, EMPTY_STRING)).join("\n"); return data || null; } function parseEventData(data) { try { return JSON.parse(data); } catch (error) { if (data.includes("\n")) { const collapsed = data.replace(/\n/g, EMPTY_STRING); try { return JSON.parse(collapsed); } catch { } } const message = error instanceof Error ? error.message : String(error); const preview = data.length > 200 ? `${data.slice(0, 200)}...` : data; throw new Error(`LLM stream parse error: ${message}. Data: ${preview}`); } } export { CommentHandler, callLlm, cleanMarkdown, createActionsPlugin, createPlugin, sanitizeLlmResponse };