@ubiquity-os/plugin-sdk 3.8.1 → 3.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -45
- package/dist/configuration.d.mts +11 -20
- package/dist/configuration.d.ts +11 -20
- package/dist/configuration.js +51 -241
- package/dist/configuration.mjs +51 -241
- package/dist/{context-BE4WjJZf.d.ts → context-BbEmsEct.d.ts} +0 -1
- package/dist/{context-Ckj1HMjz.d.mts → context-sqbr2o6i.d.mts} +0 -1
- package/dist/index.d.mts +3 -4
- package/dist/index.d.ts +3 -4
- package/dist/index.js +194 -352
- package/dist/index.mjs +193 -351
- package/dist/llm.d.mts +43 -6
- package/dist/llm.d.ts +43 -6
- package/dist/llm.js +41 -143
- package/dist/llm.mjs +41 -143
- package/dist/signature.d.mts +3 -1
- package/dist/signature.d.ts +3 -1
- package/dist/signature.js +6 -3
- package/dist/signature.mjs +6 -3
- package/package.json +35 -48
package/dist/index.mjs
CHANGED
|
@@ -1,80 +1,12 @@
|
|
|
1
1
|
// src/actions.ts
|
|
2
2
|
import * as core from "@actions/core";
|
|
3
|
+
import * as github2 from "@actions/github";
|
|
3
4
|
import { Value as Value3 } from "@sinclair/typebox/value";
|
|
4
|
-
import { Logs } from "@ubiquity-os/ubiquity-os-logger";
|
|
5
|
-
import { config } from "dotenv";
|
|
6
|
-
|
|
7
|
-
// src/error.ts
|
|
8
|
-
import { LogReturn } from "@ubiquity-os/ubiquity-os-logger";
|
|
9
|
-
function getErrorStatus(err) {
|
|
10
|
-
if (!err || typeof err !== "object") return null;
|
|
11
|
-
const candidate = err;
|
|
12
|
-
const directStatus = candidate.status ?? candidate.response?.status;
|
|
13
|
-
if (typeof directStatus === "number" && Number.isFinite(directStatus)) return directStatus;
|
|
14
|
-
if (typeof directStatus === "string" && directStatus.trim()) {
|
|
15
|
-
const parsed = Number.parseInt(directStatus, 10);
|
|
16
|
-
if (Number.isFinite(parsed)) return parsed;
|
|
17
|
-
}
|
|
18
|
-
if (err instanceof Error) {
|
|
19
|
-
const match = /LLM API error:\s*(\d{3})/i.exec(err.message);
|
|
20
|
-
if (match) {
|
|
21
|
-
const parsed = Number.parseInt(match[1], 10);
|
|
22
|
-
if (Number.isFinite(parsed)) return parsed;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
function logByStatus(context, message, metadata) {
|
|
28
|
-
const status = getErrorStatus(metadata.err);
|
|
29
|
-
const payload = { ...metadata, ...status ? { status } : {} };
|
|
30
|
-
if (status && status >= 500) return context.logger.error(message, payload);
|
|
31
|
-
if (status && status >= 400) return context.logger.warn(message, payload);
|
|
32
|
-
if (status && status >= 300) return context.logger.debug(message, payload);
|
|
33
|
-
if (status && status >= 200) return context.logger.ok(message, payload);
|
|
34
|
-
if (status && status >= 100) return context.logger.info(message, payload);
|
|
35
|
-
return context.logger.error(message, payload);
|
|
36
|
-
}
|
|
37
|
-
function transformError(context, error) {
|
|
38
|
-
if (error instanceof LogReturn) {
|
|
39
|
-
return error;
|
|
40
|
-
}
|
|
41
|
-
if (error instanceof AggregateError) {
|
|
42
|
-
const message = error.errors.map((err) => {
|
|
43
|
-
if (err instanceof LogReturn) {
|
|
44
|
-
return err.logMessage.raw;
|
|
45
|
-
}
|
|
46
|
-
if (err instanceof Error) {
|
|
47
|
-
return err.message;
|
|
48
|
-
}
|
|
49
|
-
return String(err);
|
|
50
|
-
}).join("\n\n");
|
|
51
|
-
return logByStatus(context, message, { err: error });
|
|
52
|
-
}
|
|
53
|
-
if (error instanceof Error) {
|
|
54
|
-
return logByStatus(context, error.message, { err: error });
|
|
55
|
-
}
|
|
56
|
-
return logByStatus(context, String(error), { err: error });
|
|
57
|
-
}
|
|
5
|
+
import { LogReturn as LogReturn3, Logs } from "@ubiquity-os/ubiquity-os-logger";
|
|
58
6
|
|
|
59
7
|
// src/helpers/runtime-info.ts
|
|
8
|
+
import github from "@actions/github";
|
|
60
9
|
import { getRuntimeKey } from "hono/adapter";
|
|
61
|
-
|
|
62
|
-
// src/helpers/github-context.ts
|
|
63
|
-
import * as github from "@actions/github";
|
|
64
|
-
function getGithubContext() {
|
|
65
|
-
const override = globalThis.__UOS_GITHUB_CONTEXT__;
|
|
66
|
-
if (override) {
|
|
67
|
-
return override;
|
|
68
|
-
}
|
|
69
|
-
const module = github;
|
|
70
|
-
const context = module.context ?? module.default?.context;
|
|
71
|
-
if (!context) {
|
|
72
|
-
throw new Error("GitHub context is unavailable.");
|
|
73
|
-
}
|
|
74
|
-
return context;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// src/helpers/runtime-info.ts
|
|
78
10
|
var PluginRuntimeInfo = class _PluginRuntimeInfo {
|
|
79
11
|
static _instance = null;
|
|
80
12
|
_env = {};
|
|
@@ -122,11 +54,10 @@ var CfRuntimeInfo = class extends PluginRuntimeInfo {
|
|
|
122
54
|
};
|
|
123
55
|
var NodeRuntimeInfo = class extends PluginRuntimeInfo {
|
|
124
56
|
get version() {
|
|
125
|
-
return
|
|
57
|
+
return github.context.sha;
|
|
126
58
|
}
|
|
127
59
|
get runUrl() {
|
|
128
|
-
|
|
129
|
-
return context.payload.repository ? `${context.payload.repository?.html_url}/actions/runs/${context.runId}` : "http://localhost";
|
|
60
|
+
return github.context.payload.repository ? `${github.context.payload.repository?.html_url}/actions/runs/${github.context.runId}` : "http://localhost";
|
|
130
61
|
}
|
|
131
62
|
};
|
|
132
63
|
var DenoRuntimeInfo = class extends PluginRuntimeInfo {
|
|
@@ -219,23 +150,14 @@ function getPluginOptions(options) {
|
|
|
219
150
|
}
|
|
220
151
|
|
|
221
152
|
// src/comment.ts
|
|
222
|
-
function logByStatus2(logger, message, status, metadata) {
|
|
223
|
-
const payload = { ...metadata, ...status ? { status } : {} };
|
|
224
|
-
if (status && status >= 500) return logger.error(message, payload);
|
|
225
|
-
if (status && status >= 400) return logger.warn(message, payload);
|
|
226
|
-
if (status && status >= 300) return logger.debug(message, payload);
|
|
227
|
-
if (status && status >= 200) return logger.ok(message, payload);
|
|
228
|
-
if (status && status >= 100) return logger.info(message, payload);
|
|
229
|
-
return logger.error(message, payload);
|
|
230
|
-
}
|
|
231
153
|
var CommentHandler = class _CommentHandler {
|
|
232
154
|
static HEADER_NAME = "UbiquityOS";
|
|
233
155
|
_lastCommentId = { reviewCommentId: null, issueCommentId: null };
|
|
234
|
-
async _updateIssueComment(
|
|
156
|
+
async _updateIssueComment(context2, params) {
|
|
235
157
|
if (!this._lastCommentId.issueCommentId) {
|
|
236
|
-
throw
|
|
158
|
+
throw context2.logger.error("issueCommentId is missing");
|
|
237
159
|
}
|
|
238
|
-
const commentData = await
|
|
160
|
+
const commentData = await context2.octokit.rest.issues.updateComment({
|
|
239
161
|
owner: params.owner,
|
|
240
162
|
repo: params.repo,
|
|
241
163
|
comment_id: this._lastCommentId.issueCommentId,
|
|
@@ -243,11 +165,11 @@ var CommentHandler = class _CommentHandler {
|
|
|
243
165
|
});
|
|
244
166
|
return { ...commentData.data, issueNumber: params.issueNumber };
|
|
245
167
|
}
|
|
246
|
-
async _updateReviewComment(
|
|
168
|
+
async _updateReviewComment(context2, params) {
|
|
247
169
|
if (!this._lastCommentId.reviewCommentId) {
|
|
248
|
-
throw
|
|
170
|
+
throw context2.logger.error("reviewCommentId is missing");
|
|
249
171
|
}
|
|
250
|
-
const commentData = await
|
|
172
|
+
const commentData = await context2.octokit.rest.pulls.updateReviewComment({
|
|
251
173
|
owner: params.owner,
|
|
252
174
|
repo: params.repo,
|
|
253
175
|
comment_id: this._lastCommentId.reviewCommentId,
|
|
@@ -255,9 +177,9 @@ var CommentHandler = class _CommentHandler {
|
|
|
255
177
|
});
|
|
256
178
|
return { ...commentData.data, issueNumber: params.issueNumber };
|
|
257
179
|
}
|
|
258
|
-
async _createNewComment(
|
|
180
|
+
async _createNewComment(context2, params) {
|
|
259
181
|
if (params.commentId) {
|
|
260
|
-
const commentData2 = await
|
|
182
|
+
const commentData2 = await context2.octokit.rest.pulls.createReplyForReviewComment({
|
|
261
183
|
owner: params.owner,
|
|
262
184
|
repo: params.repo,
|
|
263
185
|
pull_number: params.issueNumber,
|
|
@@ -267,7 +189,7 @@ var CommentHandler = class _CommentHandler {
|
|
|
267
189
|
this._lastCommentId.reviewCommentId = commentData2.data.id;
|
|
268
190
|
return { ...commentData2.data, issueNumber: params.issueNumber };
|
|
269
191
|
}
|
|
270
|
-
const commentData = await
|
|
192
|
+
const commentData = await context2.octokit.rest.issues.createComment({
|
|
271
193
|
owner: params.owner,
|
|
272
194
|
repo: params.repo,
|
|
273
195
|
issue_number: params.issueNumber,
|
|
@@ -276,58 +198,54 @@ var CommentHandler = class _CommentHandler {
|
|
|
276
198
|
this._lastCommentId.issueCommentId = commentData.data.id;
|
|
277
199
|
return { ...commentData.data, issueNumber: params.issueNumber };
|
|
278
200
|
}
|
|
279
|
-
_getIssueNumber(
|
|
280
|
-
if ("issue" in
|
|
281
|
-
if ("pull_request" in
|
|
282
|
-
if ("discussion" in
|
|
201
|
+
_getIssueNumber(context2) {
|
|
202
|
+
if ("issue" in context2.payload) return context2.payload.issue.number;
|
|
203
|
+
if ("pull_request" in context2.payload) return context2.payload.pull_request.number;
|
|
204
|
+
if ("discussion" in context2.payload) return context2.payload.discussion.number;
|
|
283
205
|
return void 0;
|
|
284
206
|
}
|
|
285
|
-
_getCommentId(
|
|
286
|
-
return "pull_request" in
|
|
207
|
+
_getCommentId(context2) {
|
|
208
|
+
return "pull_request" in context2.payload && "comment" in context2.payload ? context2.payload.comment.id : void 0;
|
|
287
209
|
}
|
|
288
|
-
_extractIssueContext(
|
|
289
|
-
if (!("repository" in
|
|
210
|
+
_extractIssueContext(context2) {
|
|
211
|
+
if (!("repository" in context2.payload) || !context2.payload.repository?.owner?.login) {
|
|
290
212
|
return null;
|
|
291
213
|
}
|
|
292
|
-
const issueNumber = this._getIssueNumber(
|
|
214
|
+
const issueNumber = this._getIssueNumber(context2);
|
|
293
215
|
if (!issueNumber) return null;
|
|
294
216
|
return {
|
|
295
217
|
issueNumber,
|
|
296
|
-
commentId: this._getCommentId(
|
|
297
|
-
owner:
|
|
298
|
-
repo:
|
|
218
|
+
commentId: this._getCommentId(context2),
|
|
219
|
+
owner: context2.payload.repository.owner.login,
|
|
220
|
+
repo: context2.payload.repository.name
|
|
299
221
|
};
|
|
300
222
|
}
|
|
301
|
-
_processMessage(
|
|
223
|
+
_processMessage(context2, message) {
|
|
302
224
|
if (message instanceof Error) {
|
|
303
225
|
const metadata2 = {
|
|
304
226
|
message: message.message,
|
|
305
227
|
name: message.name,
|
|
306
228
|
stack: message.stack
|
|
307
229
|
};
|
|
308
|
-
|
|
309
|
-
const logReturn = logByStatus2(context.logger, message.message, status, metadata2);
|
|
310
|
-
return { metadata: { ...metadata2, ...status ? { status } : {} }, logMessage: logReturn.logMessage };
|
|
230
|
+
return { metadata: metadata2, logMessage: context2.logger.error(message.message).logMessage };
|
|
311
231
|
}
|
|
312
|
-
const stackLine = message.metadata?.error?.stack?.split("\n")[2];
|
|
313
|
-
const callerMatch = stackLine ? /at (\S+)/.exec(stackLine) : null;
|
|
314
232
|
const metadata = message.metadata ? {
|
|
315
233
|
...message.metadata,
|
|
316
234
|
message: message.metadata.message,
|
|
317
235
|
stack: message.metadata.stack || message.metadata.error?.stack,
|
|
318
|
-
caller: message.metadata.caller ||
|
|
236
|
+
caller: message.metadata.caller || message.metadata.error?.stack?.split("\n")[2]?.match(/at (\S+)/)?.[1]
|
|
319
237
|
} : { ...message };
|
|
320
238
|
return { metadata, logMessage: message.logMessage };
|
|
321
239
|
}
|
|
322
|
-
_getInstigatorName(
|
|
323
|
-
if ("installation" in
|
|
324
|
-
return
|
|
240
|
+
_getInstigatorName(context2) {
|
|
241
|
+
if ("installation" in context2.payload && context2.payload.installation && "account" in context2.payload.installation && context2.payload.installation?.account?.name) {
|
|
242
|
+
return context2.payload.installation?.account?.name;
|
|
325
243
|
}
|
|
326
|
-
return
|
|
244
|
+
return context2.payload.sender?.login || _CommentHandler.HEADER_NAME;
|
|
327
245
|
}
|
|
328
|
-
_createMetadataContent(
|
|
246
|
+
_createMetadataContent(context2, metadata) {
|
|
329
247
|
const jsonPretty = sanitizeMetadata(metadata);
|
|
330
|
-
const instigatorName = this._getInstigatorName(
|
|
248
|
+
const instigatorName = this._getInstigatorName(context2);
|
|
331
249
|
const runUrl = PluginRuntimeInfo.getInstance().runUrl;
|
|
332
250
|
const version = PluginRuntimeInfo.getInstance().version;
|
|
333
251
|
const callingFnName = metadata.caller || "anonymous";
|
|
@@ -344,39 +262,64 @@ var CommentHandler = class _CommentHandler {
|
|
|
344
262
|
/*
|
|
345
263
|
* Creates the body for the comment, embeds the metadata and the header hidden in the body as well.
|
|
346
264
|
*/
|
|
347
|
-
createCommentBody(
|
|
348
|
-
return this._createCommentBody(
|
|
265
|
+
createCommentBody(context2, message, options) {
|
|
266
|
+
return this._createCommentBody(context2, message, options);
|
|
349
267
|
}
|
|
350
|
-
_createCommentBody(
|
|
351
|
-
const { metadata, logMessage } = this._processMessage(
|
|
352
|
-
const { header, jsonPretty } = this._createMetadataContent(
|
|
268
|
+
_createCommentBody(context2, message, options) {
|
|
269
|
+
const { metadata, logMessage } = this._processMessage(context2, message);
|
|
270
|
+
const { header, jsonPretty } = this._createMetadataContent(context2, metadata);
|
|
353
271
|
const metadataContent = this._formatMetadataContent(logMessage, header, jsonPretty);
|
|
354
272
|
return `${options?.raw ? logMessage?.raw : logMessage?.diff}
|
|
355
273
|
|
|
356
274
|
${metadataContent}
|
|
357
275
|
`;
|
|
358
276
|
}
|
|
359
|
-
async postComment(
|
|
360
|
-
const issueContext = this._extractIssueContext(
|
|
277
|
+
async postComment(context2, message, options = { updateComment: true, raw: false }) {
|
|
278
|
+
const issueContext = this._extractIssueContext(context2);
|
|
361
279
|
if (!issueContext) {
|
|
362
|
-
|
|
280
|
+
context2.logger.info("Cannot post comment: missing issue context in payload");
|
|
363
281
|
return null;
|
|
364
282
|
}
|
|
365
|
-
const body = this._createCommentBody(
|
|
283
|
+
const body = this._createCommentBody(context2, message, options);
|
|
366
284
|
const { issueNumber, commentId, owner, repo } = issueContext;
|
|
367
285
|
const params = { owner, repo, body, issueNumber };
|
|
368
286
|
if (options.updateComment) {
|
|
369
|
-
if (this._lastCommentId.issueCommentId && !("pull_request" in
|
|
370
|
-
return this._updateIssueComment(
|
|
287
|
+
if (this._lastCommentId.issueCommentId && !("pull_request" in context2.payload && "comment" in context2.payload)) {
|
|
288
|
+
return this._updateIssueComment(context2, params);
|
|
371
289
|
}
|
|
372
|
-
if (this._lastCommentId.reviewCommentId && "pull_request" in
|
|
373
|
-
return this._updateReviewComment(
|
|
290
|
+
if (this._lastCommentId.reviewCommentId && "pull_request" in context2.payload && "comment" in context2.payload) {
|
|
291
|
+
return this._updateReviewComment(context2, params);
|
|
374
292
|
}
|
|
375
293
|
}
|
|
376
|
-
return this._createNewComment(
|
|
294
|
+
return this._createNewComment(context2, { ...params, commentId });
|
|
377
295
|
}
|
|
378
296
|
};
|
|
379
297
|
|
|
298
|
+
// src/error.ts
|
|
299
|
+
import { LogReturn as LogReturn2 } from "@ubiquity-os/ubiquity-os-logger";
|
|
300
|
+
function transformError(context2, error) {
|
|
301
|
+
let loggerError;
|
|
302
|
+
if (error instanceof AggregateError) {
|
|
303
|
+
loggerError = context2.logger.error(
|
|
304
|
+
error.errors.map((err) => {
|
|
305
|
+
if (err instanceof LogReturn2) {
|
|
306
|
+
return err.logMessage.raw;
|
|
307
|
+
} else if (err instanceof Error) {
|
|
308
|
+
return err.message;
|
|
309
|
+
} else {
|
|
310
|
+
return err;
|
|
311
|
+
}
|
|
312
|
+
}).join("\n\n"),
|
|
313
|
+
{ error }
|
|
314
|
+
);
|
|
315
|
+
} else if (error instanceof Error || error instanceof LogReturn2) {
|
|
316
|
+
loggerError = error;
|
|
317
|
+
} else {
|
|
318
|
+
loggerError = context2.logger.error(String(error));
|
|
319
|
+
}
|
|
320
|
+
return loggerError;
|
|
321
|
+
}
|
|
322
|
+
|
|
380
323
|
// src/helpers/command.ts
|
|
381
324
|
import { Value } from "@sinclair/typebox/value";
|
|
382
325
|
function getCommand(inputs, pluginOptions) {
|
|
@@ -447,7 +390,7 @@ async function verifySignature(publicKeyPem, inputs, signature) {
|
|
|
447
390
|
ref: inputs.ref,
|
|
448
391
|
command: inputs.command
|
|
449
392
|
};
|
|
450
|
-
const pemContents = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").
|
|
393
|
+
const pemContents = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").trim();
|
|
451
394
|
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
452
395
|
const publicKey = await crypto.subtle.importKey(
|
|
453
396
|
"spki",
|
|
@@ -499,13 +442,16 @@ var inputSchema = T2.Object({
|
|
|
499
442
|
});
|
|
500
443
|
|
|
501
444
|
// src/actions.ts
|
|
502
|
-
|
|
503
|
-
async function handleError(context, pluginOptions, error) {
|
|
445
|
+
async function handleError(context2, pluginOptions, error) {
|
|
504
446
|
console.error(error);
|
|
505
|
-
const loggerError = transformError(
|
|
506
|
-
|
|
447
|
+
const loggerError = transformError(context2, error);
|
|
448
|
+
if (loggerError instanceof LogReturn3) {
|
|
449
|
+
core.setFailed(loggerError.logMessage.diff);
|
|
450
|
+
} else if (loggerError instanceof Error) {
|
|
451
|
+
core.setFailed(loggerError);
|
|
452
|
+
}
|
|
507
453
|
if (pluginOptions.postCommentOnError && loggerError) {
|
|
508
|
-
await
|
|
454
|
+
await context2.commentHandler.postComment(context2, loggerError);
|
|
509
455
|
}
|
|
510
456
|
}
|
|
511
457
|
async function createActionsPlugin(handler, options) {
|
|
@@ -515,8 +461,7 @@ async function createActionsPlugin(handler, options) {
|
|
|
515
461
|
core.setFailed("Error: PLUGIN_GITHUB_TOKEN env is not set");
|
|
516
462
|
return;
|
|
517
463
|
}
|
|
518
|
-
const
|
|
519
|
-
const body = githubContext.payload.inputs;
|
|
464
|
+
const body = github2.context.payload.inputs;
|
|
520
465
|
const inputSchemaErrors = [...Value3.Errors(inputSchema, body)];
|
|
521
466
|
if (inputSchemaErrors.length) {
|
|
522
467
|
console.dir(inputSchemaErrors, { depth: null });
|
|
@@ -529,17 +474,17 @@ async function createActionsPlugin(handler, options) {
|
|
|
529
474
|
return;
|
|
530
475
|
}
|
|
531
476
|
const inputs = Value3.Decode(inputSchema, body);
|
|
532
|
-
let
|
|
477
|
+
let config;
|
|
533
478
|
if (pluginOptions.settingsSchema) {
|
|
534
479
|
try {
|
|
535
|
-
|
|
480
|
+
config = Value3.Decode(pluginOptions.settingsSchema, Value3.Default(pluginOptions.settingsSchema, inputs.settings));
|
|
536
481
|
} catch (e) {
|
|
537
482
|
console.dir(...Value3.Errors(pluginOptions.settingsSchema, inputs.settings), { depth: null });
|
|
538
483
|
core.setFailed(`Error: Invalid settings provided.`);
|
|
539
484
|
throw e;
|
|
540
485
|
}
|
|
541
486
|
} else {
|
|
542
|
-
|
|
487
|
+
config = inputs.settings;
|
|
543
488
|
}
|
|
544
489
|
let env;
|
|
545
490
|
if (pluginOptions.envSchema) {
|
|
@@ -554,34 +499,33 @@ async function createActionsPlugin(handler, options) {
|
|
|
554
499
|
env = process.env;
|
|
555
500
|
}
|
|
556
501
|
const command = getCommand(inputs, pluginOptions);
|
|
557
|
-
const
|
|
502
|
+
const context2 = {
|
|
558
503
|
eventName: inputs.eventName,
|
|
559
504
|
payload: inputs.eventPayload,
|
|
560
505
|
command,
|
|
561
506
|
authToken: inputs.authToken,
|
|
562
507
|
ubiquityKernelToken: inputs.ubiquityKernelToken,
|
|
563
508
|
octokit: new customOctokit({ auth: inputs.authToken }),
|
|
564
|
-
config
|
|
509
|
+
config,
|
|
565
510
|
env,
|
|
566
511
|
logger: new Logs(pluginOptions.logLevel),
|
|
567
512
|
commentHandler: new CommentHandler()
|
|
568
513
|
};
|
|
569
514
|
try {
|
|
570
|
-
const result = await handler(
|
|
515
|
+
const result = await handler(context2);
|
|
571
516
|
core.setOutput("result", result);
|
|
572
517
|
if (pluginOptions?.returnDataToKernel) {
|
|
573
518
|
await returnDataToKernel(pluginGithubToken, inputs.stateId, result);
|
|
574
519
|
}
|
|
575
520
|
} catch (error) {
|
|
576
|
-
await handleError(
|
|
521
|
+
await handleError(context2, pluginOptions, error);
|
|
577
522
|
}
|
|
578
523
|
}
|
|
579
524
|
async function returnDataToKernel(repoToken, stateId, output) {
|
|
580
|
-
const githubContext = getGithubContext();
|
|
581
525
|
const octokit = new customOctokit({ auth: repoToken });
|
|
582
526
|
await octokit.rest.repos.createDispatchEvent({
|
|
583
|
-
owner:
|
|
584
|
-
repo:
|
|
527
|
+
owner: github2.context.repo.owner,
|
|
528
|
+
repo: github2.context.repo.repo,
|
|
585
529
|
event_type: "return-data-to-ubiquity-os-kernel",
|
|
586
530
|
client_payload: {
|
|
587
531
|
state_id: stateId,
|
|
@@ -643,17 +587,107 @@ function processSegment(segment, extraTags, shouldCollapseEmptyLines) {
|
|
|
643
587
|
return s;
|
|
644
588
|
}
|
|
645
589
|
|
|
590
|
+
// src/llm/index.ts
|
|
591
|
+
function normalizeBaseUrl(baseUrl) {
|
|
592
|
+
let normalized = baseUrl.trim();
|
|
593
|
+
while (normalized.endsWith("/")) {
|
|
594
|
+
normalized = normalized.slice(0, -1);
|
|
595
|
+
}
|
|
596
|
+
return normalized;
|
|
597
|
+
}
|
|
598
|
+
function getEnvString(name) {
|
|
599
|
+
if (typeof process === "undefined" || !process?.env) return "";
|
|
600
|
+
return String(process.env[name] ?? "").trim();
|
|
601
|
+
}
|
|
602
|
+
function getAiBaseUrl(options) {
|
|
603
|
+
if (typeof options.baseUrl === "string" && options.baseUrl.trim()) {
|
|
604
|
+
return normalizeBaseUrl(options.baseUrl);
|
|
605
|
+
}
|
|
606
|
+
const envBaseUrl = getEnvString("UBQ_AI_BASE_URL") || getEnvString("UBQ_AI_URL");
|
|
607
|
+
if (envBaseUrl) return normalizeBaseUrl(envBaseUrl);
|
|
608
|
+
return "https://ai.ubq.fi";
|
|
609
|
+
}
|
|
610
|
+
async function callLlm(options, input) {
|
|
611
|
+
const inputPayload = input;
|
|
612
|
+
const authToken = inputPayload.authToken;
|
|
613
|
+
const ubiquityKernelToken = inputPayload.ubiquityKernelToken;
|
|
614
|
+
const payload = inputPayload.payload ?? inputPayload.eventPayload;
|
|
615
|
+
const owner = payload?.repository?.owner?.login ?? "";
|
|
616
|
+
const repo = payload?.repository?.name ?? "";
|
|
617
|
+
const installationId = payload?.installation?.id;
|
|
618
|
+
if (!authToken) throw new Error("Missing authToken in inputs");
|
|
619
|
+
const isKernelTokenRequired = authToken.trim().startsWith("gh");
|
|
620
|
+
if (isKernelTokenRequired && !ubiquityKernelToken) {
|
|
621
|
+
throw new Error("Missing ubiquityKernelToken in inputs (kernel attestation is required for GitHub auth)");
|
|
622
|
+
}
|
|
623
|
+
const { baseUrl, model, stream: isStream, messages, ...rest } = options;
|
|
624
|
+
const url = `${getAiBaseUrl({ ...options, baseUrl })}/v1/chat/completions`;
|
|
625
|
+
const body = JSON.stringify({
|
|
626
|
+
...rest,
|
|
627
|
+
...model ? { model } : {},
|
|
628
|
+
messages,
|
|
629
|
+
stream: isStream ?? false
|
|
630
|
+
});
|
|
631
|
+
const headers = {
|
|
632
|
+
Authorization: `Bearer ${authToken}`,
|
|
633
|
+
"Content-Type": "application/json"
|
|
634
|
+
};
|
|
635
|
+
if (owner) headers["X-GitHub-Owner"] = owner;
|
|
636
|
+
if (repo) headers["X-GitHub-Repo"] = repo;
|
|
637
|
+
if (typeof installationId === "number" && Number.isFinite(installationId)) {
|
|
638
|
+
headers["X-GitHub-Installation-Id"] = String(installationId);
|
|
639
|
+
}
|
|
640
|
+
if (ubiquityKernelToken) {
|
|
641
|
+
headers["X-Ubiquity-Kernel-Token"] = ubiquityKernelToken;
|
|
642
|
+
}
|
|
643
|
+
const response = await fetch(url, { method: "POST", headers, body });
|
|
644
|
+
if (!response.ok) {
|
|
645
|
+
const err = await response.text();
|
|
646
|
+
throw new Error(`LLM API error: ${response.status} - ${err}`);
|
|
647
|
+
}
|
|
648
|
+
if (isStream) {
|
|
649
|
+
if (!response.body) {
|
|
650
|
+
throw new Error("LLM API error: missing response body for streaming request");
|
|
651
|
+
}
|
|
652
|
+
return parseSseStream(response.body);
|
|
653
|
+
}
|
|
654
|
+
return response.json();
|
|
655
|
+
}
|
|
656
|
+
async function* parseSseStream(body) {
|
|
657
|
+
const reader = body.getReader();
|
|
658
|
+
const decoder = new TextDecoder();
|
|
659
|
+
let buffer = "";
|
|
660
|
+
try {
|
|
661
|
+
while (true) {
|
|
662
|
+
const { value, done: isDone } = await reader.read();
|
|
663
|
+
if (isDone) break;
|
|
664
|
+
buffer += decoder.decode(value, { stream: true });
|
|
665
|
+
const events = buffer.split("\n\n");
|
|
666
|
+
buffer = events.pop() || "";
|
|
667
|
+
for (const event of events) {
|
|
668
|
+
if (event.startsWith("data: ")) {
|
|
669
|
+
const data = event.slice(6);
|
|
670
|
+
if (data === "[DONE]") return;
|
|
671
|
+
yield JSON.parse(data);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
} finally {
|
|
676
|
+
reader.releaseLock();
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
646
680
|
// src/server.ts
|
|
647
681
|
import { Value as Value4 } from "@sinclair/typebox/value";
|
|
648
682
|
import { Logs as Logs2 } from "@ubiquity-os/ubiquity-os-logger";
|
|
649
683
|
import { Hono } from "hono";
|
|
650
684
|
import { env as honoEnv } from "hono/adapter";
|
|
651
685
|
import { HTTPException } from "hono/http-exception";
|
|
652
|
-
async function handleError2(
|
|
686
|
+
async function handleError2(context2, pluginOptions, error) {
|
|
653
687
|
console.error(error);
|
|
654
|
-
const loggerError = transformError(
|
|
688
|
+
const loggerError = transformError(context2, error);
|
|
655
689
|
if (pluginOptions.postCommentOnError && loggerError) {
|
|
656
|
-
await
|
|
690
|
+
await context2.commentHandler.postComment(context2, loggerError);
|
|
657
691
|
}
|
|
658
692
|
throw new HTTPException(500, { message: "Unexpected error" });
|
|
659
693
|
}
|
|
@@ -678,16 +712,16 @@ function createPlugin(handler, manifest, options) {
|
|
|
678
712
|
throw new HTTPException(400, { message: "Invalid signature" });
|
|
679
713
|
}
|
|
680
714
|
const inputs = Value4.Decode(inputSchema, body);
|
|
681
|
-
let
|
|
715
|
+
let config;
|
|
682
716
|
if (pluginOptions.settingsSchema) {
|
|
683
717
|
try {
|
|
684
|
-
|
|
718
|
+
config = Value4.Decode(pluginOptions.settingsSchema, Value4.Default(pluginOptions.settingsSchema, inputs.settings));
|
|
685
719
|
} catch (e) {
|
|
686
720
|
console.dir(...Value4.Errors(pluginOptions.settingsSchema, inputs.settings), { depth: null });
|
|
687
721
|
throw e;
|
|
688
722
|
}
|
|
689
723
|
} else {
|
|
690
|
-
|
|
724
|
+
config = inputs.settings;
|
|
691
725
|
}
|
|
692
726
|
let env;
|
|
693
727
|
const honoEnvironment = honoEnv(ctx);
|
|
@@ -704,219 +738,27 @@ function createPlugin(handler, manifest, options) {
|
|
|
704
738
|
const workerName = new URL(inputs.ref).hostname.split(".")[0];
|
|
705
739
|
PluginRuntimeInfo.getInstance({ ...env, CLOUDFLARE_WORKER_NAME: workerName });
|
|
706
740
|
const command = getCommand(inputs, pluginOptions);
|
|
707
|
-
const
|
|
741
|
+
const context2 = {
|
|
708
742
|
eventName: inputs.eventName,
|
|
709
743
|
payload: inputs.eventPayload,
|
|
710
744
|
command,
|
|
711
745
|
authToken: inputs.authToken,
|
|
712
746
|
ubiquityKernelToken: inputs.ubiquityKernelToken,
|
|
713
747
|
octokit: new customOctokit({ auth: inputs.authToken }),
|
|
714
|
-
config
|
|
748
|
+
config,
|
|
715
749
|
env,
|
|
716
750
|
logger: new Logs2(pluginOptions.logLevel),
|
|
717
751
|
commentHandler: new CommentHandler()
|
|
718
752
|
};
|
|
719
753
|
try {
|
|
720
|
-
const result = await handler(
|
|
754
|
+
const result = await handler(context2);
|
|
721
755
|
return ctx.json({ stateId: inputs.stateId, output: result ?? {} });
|
|
722
756
|
} catch (error) {
|
|
723
|
-
await handleError2(
|
|
757
|
+
await handleError2(context2, pluginOptions, error);
|
|
724
758
|
}
|
|
725
759
|
});
|
|
726
760
|
return app;
|
|
727
761
|
}
|
|
728
|
-
|
|
729
|
-
// src/llm/index.ts
|
|
730
|
-
var EMPTY_STRING = "";
|
|
731
|
-
function normalizeBaseUrl(baseUrl) {
|
|
732
|
-
let normalized = baseUrl.trim();
|
|
733
|
-
while (normalized.endsWith("/")) {
|
|
734
|
-
normalized = normalized.slice(0, -1);
|
|
735
|
-
}
|
|
736
|
-
return normalized;
|
|
737
|
-
}
|
|
738
|
-
var MAX_LLM_RETRIES = 2;
|
|
739
|
-
var RETRY_BACKOFF_MS = [250, 750];
|
|
740
|
-
function getRetryDelayMs(attempt) {
|
|
741
|
-
return RETRY_BACKOFF_MS[Math.min(attempt, RETRY_BACKOFF_MS.length - 1)] ?? 750;
|
|
742
|
-
}
|
|
743
|
-
function sleep(ms) {
|
|
744
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
745
|
-
}
|
|
746
|
-
function getEnvString(name) {
|
|
747
|
-
if (typeof process === "undefined" || !process?.env) return EMPTY_STRING;
|
|
748
|
-
return String(process.env[name] ?? EMPTY_STRING).trim();
|
|
749
|
-
}
|
|
750
|
-
function getAiBaseUrl(options) {
|
|
751
|
-
if (typeof options.baseUrl === "string" && options.baseUrl.trim()) {
|
|
752
|
-
return normalizeBaseUrl(options.baseUrl);
|
|
753
|
-
}
|
|
754
|
-
const envBaseUrl = getEnvString("UOS_AI_URL") || getEnvString("UOS_AI_BASE_URL");
|
|
755
|
-
if (envBaseUrl) return normalizeBaseUrl(envBaseUrl);
|
|
756
|
-
return "https://ai-ubq-fi.deno.dev";
|
|
757
|
-
}
|
|
758
|
-
async function callLlm(options, input) {
|
|
759
|
-
const authToken = String(input.authToken ?? EMPTY_STRING).trim();
|
|
760
|
-
if (!authToken) {
|
|
761
|
-
const err = new Error("Missing authToken in input");
|
|
762
|
-
err.status = 401;
|
|
763
|
-
throw err;
|
|
764
|
-
}
|
|
765
|
-
const kernelToken = "ubiquityKernelToken" in input ? input.ubiquityKernelToken : void 0;
|
|
766
|
-
const payload = getPayload(input);
|
|
767
|
-
const { owner, repo, installationId } = getRepoMetadata(payload);
|
|
768
|
-
ensureKernelToken(authToken, kernelToken);
|
|
769
|
-
const { baseUrl, model, stream: isStream, messages, ...rest } = options;
|
|
770
|
-
ensureMessages(messages);
|
|
771
|
-
const url = buildAiUrl(options, baseUrl);
|
|
772
|
-
const body = JSON.stringify({
|
|
773
|
-
...rest,
|
|
774
|
-
...model ? { model } : {},
|
|
775
|
-
messages,
|
|
776
|
-
stream: isStream ?? false
|
|
777
|
-
});
|
|
778
|
-
const headers = buildHeaders(authToken, {
|
|
779
|
-
owner,
|
|
780
|
-
repo,
|
|
781
|
-
installationId,
|
|
782
|
-
ubiquityKernelToken: kernelToken
|
|
783
|
-
});
|
|
784
|
-
const response = await fetchWithRetry(url, { method: "POST", headers, body }, MAX_LLM_RETRIES);
|
|
785
|
-
if (isStream) {
|
|
786
|
-
if (!response.body) {
|
|
787
|
-
throw new Error("LLM API error: missing response body for streaming request");
|
|
788
|
-
}
|
|
789
|
-
return parseSseStream(response.body);
|
|
790
|
-
}
|
|
791
|
-
return response.json();
|
|
792
|
-
}
|
|
793
|
-
function ensureKernelToken(authToken, kernelToken) {
|
|
794
|
-
const isKernelTokenRequired = authToken.startsWith("gh");
|
|
795
|
-
if (isKernelTokenRequired && !kernelToken) {
|
|
796
|
-
const err = new Error("Missing ubiquityKernelToken in input (kernel attestation is required for GitHub auth)");
|
|
797
|
-
err.status = 401;
|
|
798
|
-
throw err;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
function ensureMessages(messages) {
|
|
802
|
-
if (!Array.isArray(messages) || messages.length === 0) {
|
|
803
|
-
const err = new Error("messages must be a non-empty array");
|
|
804
|
-
err.status = 400;
|
|
805
|
-
throw err;
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
function buildAiUrl(options, baseUrl) {
|
|
809
|
-
return `${getAiBaseUrl({ ...options, baseUrl })}/v1/chat/completions`;
|
|
810
|
-
}
|
|
811
|
-
async function fetchWithRetry(url, options, maxRetries) {
|
|
812
|
-
let attempt = 0;
|
|
813
|
-
let lastError;
|
|
814
|
-
while (attempt <= maxRetries) {
|
|
815
|
-
try {
|
|
816
|
-
const response = await fetch(url, options);
|
|
817
|
-
if (response.ok) return response;
|
|
818
|
-
const errText = await response.text();
|
|
819
|
-
if (response.status >= 500 && attempt < maxRetries) {
|
|
820
|
-
await sleep(getRetryDelayMs(attempt));
|
|
821
|
-
attempt += 1;
|
|
822
|
-
continue;
|
|
823
|
-
}
|
|
824
|
-
const error = new Error(`LLM API error: ${response.status} - ${errText}`);
|
|
825
|
-
error.status = response.status;
|
|
826
|
-
throw error;
|
|
827
|
-
} catch (error) {
|
|
828
|
-
lastError = error;
|
|
829
|
-
const status = typeof error.status === "number" ? error.status : void 0;
|
|
830
|
-
if (typeof status === "number" && status < 500) {
|
|
831
|
-
throw error;
|
|
832
|
-
}
|
|
833
|
-
if (attempt >= maxRetries) throw error;
|
|
834
|
-
await sleep(getRetryDelayMs(attempt));
|
|
835
|
-
attempt += 1;
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
throw lastError ?? new Error("LLM API error: request failed after retries");
|
|
839
|
-
}
|
|
840
|
-
function getPayload(input) {
|
|
841
|
-
if ("payload" in input) {
|
|
842
|
-
return input.payload;
|
|
843
|
-
}
|
|
844
|
-
return input.eventPayload;
|
|
845
|
-
}
|
|
846
|
-
function getRepoMetadata(payload) {
|
|
847
|
-
const repoPayload = payload;
|
|
848
|
-
return {
|
|
849
|
-
owner: repoPayload?.repository?.owner?.login ?? EMPTY_STRING,
|
|
850
|
-
repo: repoPayload?.repository?.name ?? EMPTY_STRING,
|
|
851
|
-
installationId: repoPayload?.installation?.id
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
function buildHeaders(authToken, options) {
|
|
855
|
-
const headers = {
|
|
856
|
-
Authorization: `Bearer ${authToken}`,
|
|
857
|
-
"Content-Type": "application/json"
|
|
858
|
-
};
|
|
859
|
-
if (options.owner) headers["X-GitHub-Owner"] = options.owner;
|
|
860
|
-
if (options.repo) headers["X-GitHub-Repo"] = options.repo;
|
|
861
|
-
if (typeof options.installationId === "number" && Number.isFinite(options.installationId)) {
|
|
862
|
-
headers["X-GitHub-Installation-Id"] = String(options.installationId);
|
|
863
|
-
}
|
|
864
|
-
if (options.ubiquityKernelToken) {
|
|
865
|
-
headers["X-Ubiquity-Kernel-Token"] = options.ubiquityKernelToken;
|
|
866
|
-
}
|
|
867
|
-
return headers;
|
|
868
|
-
}
|
|
869
|
-
async function* parseSseStream(body) {
|
|
870
|
-
const reader = body.getReader();
|
|
871
|
-
const decoder = new TextDecoder();
|
|
872
|
-
let buffer = EMPTY_STRING;
|
|
873
|
-
try {
|
|
874
|
-
while (true) {
|
|
875
|
-
const { value, done: isDone } = await reader.read();
|
|
876
|
-
if (isDone) break;
|
|
877
|
-
buffer += decoder.decode(value, { stream: true });
|
|
878
|
-
const { events, remainder } = splitSseEvents(buffer);
|
|
879
|
-
buffer = remainder;
|
|
880
|
-
for (const event of events) {
|
|
881
|
-
const data = getEventData(event);
|
|
882
|
-
if (!data) continue;
|
|
883
|
-
if (data.trim() === "[DONE]") return;
|
|
884
|
-
yield parseEventData(data);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
} finally {
|
|
888
|
-
reader.releaseLock();
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
function splitSseEvents(buffer) {
|
|
892
|
-
const normalized = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
893
|
-
const parts = normalized.split("\n\n");
|
|
894
|
-
const remainder = parts.pop() ?? EMPTY_STRING;
|
|
895
|
-
return { events: parts, remainder };
|
|
896
|
-
}
|
|
897
|
-
function getEventData(event) {
|
|
898
|
-
if (!event.trim()) return null;
|
|
899
|
-
const dataLines = event.split("\n").filter((line) => line.startsWith("data:"));
|
|
900
|
-
if (!dataLines.length) return null;
|
|
901
|
-
const data = dataLines.map((line) => line.startsWith("data: ") ? line.slice(6) : line.slice(5).replace(/^ /, EMPTY_STRING)).join("\n");
|
|
902
|
-
return data || null;
|
|
903
|
-
}
|
|
904
|
-
function parseEventData(data) {
|
|
905
|
-
try {
|
|
906
|
-
return JSON.parse(data);
|
|
907
|
-
} catch (error) {
|
|
908
|
-
if (data.includes("\n")) {
|
|
909
|
-
const collapsed = data.replace(/\n/g, EMPTY_STRING);
|
|
910
|
-
try {
|
|
911
|
-
return JSON.parse(collapsed);
|
|
912
|
-
} catch {
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
916
|
-
const preview = data.length > 200 ? `${data.slice(0, 200)}...` : data;
|
|
917
|
-
throw new Error(`LLM stream parse error: ${message}. Data: ${preview}`);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
762
|
export {
|
|
921
763
|
CommentHandler,
|
|
922
764
|
callLlm,
|