UNPKG

@ubiquity-os/plugin-sdk

Version:

SDK for plugin support.

1,312 lines (1,285 loc) 50.2 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { CommentHandler: () => CommentHandler, callLlm: () => callLlm, cleanMarkdown: () => cleanMarkdown, createActionsPlugin: () => createActionsPlugin, createPlugin: () => createPlugin, sanitizeLlmResponse: () => sanitizeLlmResponse }); module.exports = __toCommonJS(src_exports); // src/actions.ts var core = __toESM(require("@actions/core")); var import_value3 = require("@sinclair/typebox/value"); var import_ubiquity_os_logger3 = require("@ubiquity-os/ubiquity-os-logger"); // src/error.ts var import_ubiquity_os_logger = require("@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 import_ubiquity_os_logger.LogReturn) { return error; } if (error instanceof AggregateError) { const message = error.errors.map((err) => { if (err instanceof import_ubiquity_os_logger.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 var import_adapter = require("hono/adapter"); // src/helpers/github-context.ts var github = __toESM(require("@actions/github")); function getGithubContext() { const override = globalThis.__UOS_GITHUB_CONTEXT__; if (override) { return override; } const module2 = github; const context = module2.context ?? module2.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 ((0, import_adapter.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 var import_ubiquity_os_logger2 = require("@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 || import_ubiquity_os_logger2.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 var import_value = require("@sinclair/typebox/value"); function getCommand(inputs, pluginOptions) { let command = null; if (inputs.command && pluginOptions.commandSchema) { try { command = import_value.Value.Decode(pluginOptions.commandSchema, import_value.Value.Default(pluginOptions.commandSchema, inputs.command)); } catch (e) { console.dir(...import_value.Value.Errors(pluginOptions.commandSchema, inputs.command), { depth: null }); throw e; } } else if (inputs.command) { command = inputs.command; } return command; } // src/helpers/compression.ts var import_node_zlib = require("node:zlib"); function compressString(str) { const input = Buffer.from(str, "utf8"); const compressed = (0, import_node_zlib.brotliCompressSync)(input); return Buffer.from(compressed).toString("base64"); } function decompressString(compressed) { const buffer = Buffer.from(compressed, "base64"); const decompressed = (0, import_node_zlib.brotliDecompressSync)(buffer); return Buffer.from(decompressed).toString("utf8"); } // src/octokit.ts var import_core = require("@octokit/core"); var import_plugin_paginate_graphql = require("@octokit/plugin-paginate-graphql"); var import_plugin_paginate_rest = require("@octokit/plugin-paginate-rest"); var import_plugin_rest_endpoint_methods = require("@octokit/plugin-rest-endpoint-methods"); var import_plugin_retry = require("@octokit/plugin-retry"); var import_plugin_throttling = require("@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 = import_core.Octokit.plugin(import_plugin_throttling.throttling, import_plugin_retry.retry, import_plugin_paginate_rest.paginateRest, import_plugin_rest_endpoint_methods.restEndpointMethods, import_plugin_paginate_graphql.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 var import_typebox3 = require("@sinclair/typebox"); // src/types/command.ts var import_typebox = require("@sinclair/typebox"); var commandCallSchema = import_typebox.Type.Union([import_typebox.Type.Null(), import_typebox.Type.Object({ name: import_typebox.Type.String(), parameters: import_typebox.Type.Unknown() })]); // src/types/util.ts var import_typebox2 = require("@sinclair/typebox"); var import_value2 = require("@sinclair/typebox/value"); function jsonType(type, decompress = false) { return import_typebox2.Type.Transform(import_typebox2.Type.String()).Decode((value) => { const parsed = JSON.parse(decompress ? decompressString(value) : value); return import_value2.Value.Decode(type, import_value2.Value.Default(type, parsed)); }).Encode((value) => JSON.stringify(value)); } // src/types/input-schema.ts var inputSchema = import_typebox3.Type.Object({ stateId: import_typebox3.Type.String(), eventName: import_typebox3.Type.String(), eventPayload: jsonType(import_typebox3.Type.Record(import_typebox3.Type.String(), import_typebox3.Type.Any()), true), command: jsonType(commandCallSchema), authToken: import_typebox3.Type.String(), ubiquityKernelToken: import_typebox3.Type.Optional(import_typebox3.Type.String()), settings: jsonType(import_typebox3.Type.Record(import_typebox3.Type.String(), import_typebox3.Type.Any())), ref: import_typebox3.Type.String(), signature: import_typebox3.Type.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 = [...import_value3.Value.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 import_value3.Value.Decode(inputSchema, body); } function decodeWithSchema(schema, value, errorMessage) { if (!schema) { return { value }; } try { return { value: import_value3.Value.Decode(schema, import_value3.Value.Default(schema, value)) }; } catch (error) { console.dir(...import_value3.Value.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 import_ubiquity_os_logger3.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 var import_value4 = require("@sinclair/typebox/value"); var import_ubiquity_os_logger4 = require("@ubiquity-os/ubiquity-os-logger"); var import_hono = require("hono"); var import_adapter2 = require("hono/adapter"); var import_http_exception = require("hono/http-exception"); // src/types/manifest.ts var import_typebox4 = require("@sinclair/typebox"); var import_webhooks = require("@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 = import_typebox4.Type.Union(import_webhooks.emitterEventNames.map((o) => import_typebox4.Type.Literal(o))); var exampleCommandExecutionSchema = import_typebox4.Type.Object({ commandInvocation: import_typebox4.Type.String({ minLength: 1 }), githubContext: import_typebox4.Type.Optional(import_typebox4.Type.Record(import_typebox4.Type.String(), import_typebox4.Type.Any())), expectedToolCallResult: import_typebox4.Type.Object({ function: import_typebox4.Type.String({ minLength: 1 }), parameters: import_typebox4.Type.Record(import_typebox4.Type.String(), import_typebox4.Type.Any()) }) }); var commandSchema = import_typebox4.Type.Object({ description: import_typebox4.Type.String({ minLength: 1 }), "ubiquity:example": import_typebox4.Type.String({ minLength: 1 }), parameters: import_typebox4.Type.Optional(import_typebox4.Type.Record(import_typebox4.Type.String(), import_typebox4.Type.Any())), examples: import_typebox4.Type.Optional(import_typebox4.Type.Array(exampleCommandExecutionSchema, { default: [] })) }); var manifestSchema = import_typebox4.Type.Object({ name: import_typebox4.Type.String({ minLength: 1 }), short_name: import_typebox4.Type.String({ minLength: 1 }), description: import_typebox4.Type.Optional(import_typebox4.Type.String({ default: "" })), commands: import_typebox4.Type.Optional(import_typebox4.Type.Record(import_typebox4.Type.String({ pattern: "^[A-Za-z-_]+$" }), commandSchema, { default: {} })), "ubiquity:listeners": import_typebox4.Type.Optional(import_typebox4.Type.Array(runEvent, { default: [] })), configuration: import_typebox4.Type.Optional(import_typebox4.Type.Record(import_typebox4.Type.String(), import_typebox4.Type.Any(), { default: {} })), skipBotEvents: import_typebox4.Type.Optional(import_typebox4.Type.Boolean({ default: true })), homepage_url: import_typebox4.Type.Optional(import_typebox4.Type.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 import_http_exception.HTTPException(500, { message: "Unexpected error" }); } function createPlugin(handler, manifest, options) { const pluginOptions = getPluginOptions(options); const app = new import_hono.Hono(); app.get("/manifest.json", (ctx) => { return ctx.json(resolveRuntimeManifest(manifest, (0, import_adapter2.env)(ctx))); }); app.post("/", async function appPost(ctx) { if (ctx.req.header("content-type") !== "application/json") { throw new import_http_exception.HTTPException(400, { message: "Content-Type must be application/json" }); } const body = await ctx.req.json(); const inputSchemaErrors = [...import_value4.Value.Errors(inputSchema, body)]; if (inputSchemaErrors.length) { console.dir(inputSchemaErrors, { depth: null }); throw new import_http_exception.HTTPException(400, { message: "Invalid body" }); } const signature = body.signature; if (!pluginOptions.bypassSignatureVerification && !await verifySignature(pluginOptions.kernelPublicKey, body, signature)) { throw new import_http_exception.HTTPException(400, { message: "Invalid signature" }); } const inputs = import_value4.Value.Decode(inputSchema, body); let config; if (pluginOptions.settingsSchema) { try { config = import_value4.Value.Decode(pluginOptions.settingsSchema, import_value4.Value.Default(pluginOptions.settingsSchema, inputs.settings)); } catch (e) { console.dir(...import_value4.Value.Errors(pluginOptions.settingsSchema, inputs.settings), { depth: null }); throw e; } } else { config = inputs.settings; } let env; const honoEnvironment = (0, import_adapter2.env)(ctx); if (pluginOptions.envSchema) { try { env = import_value4.Value.Decode(pluginOptions.envSchema, import_value4.Value.Default(pluginOptions.envSchema, honoEnvironment)); } catch (e) { console.dir(...import_value4.Value.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 import_ubiquity_os_logger4.Logs(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 pars