@ubiquity-os/plugin-sdk
Version:
SDK for plugin support.
1,312 lines (1,285 loc) • 50.2 kB
JavaScript
"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, "<").replace(/>/g, ">").replace(/--/g, "--");
}
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