@ubiquity-os/plugin-sdk 3.8.4 → 3.9.0
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 +47 -10
- package/dist/configuration.d.mts +42 -19
- package/dist/configuration.d.ts +42 -19
- package/dist/configuration.js +430 -92
- package/dist/configuration.mjs +428 -91
- package/dist/{context-sqbr2o6i.d.mts → context-Dwl3aRX-.d.mts} +29 -0
- package/dist/{context-BbEmsEct.d.ts → context-zLHgu52i.d.ts} +29 -0
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +575 -224
- package/dist/index.mjs +574 -223
- package/dist/llm.d.mts +7 -43
- package/dist/llm.d.ts +7 -43
- package/dist/llm.js +175 -43
- package/dist/llm.mjs +175 -43
- package/dist/manifest.d.mts +2 -2
- package/dist/manifest.d.ts +2 -2
- package/dist/signature.js +3 -2
- package/dist/signature.mjs +3 -2
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -41,13 +41,80 @@ module.exports = __toCommonJS(src_exports);
|
|
|
41
41
|
|
|
42
42
|
// src/actions.ts
|
|
43
43
|
var core = __toESM(require("@actions/core"));
|
|
44
|
-
var github2 = __toESM(require("@actions/github"));
|
|
45
44
|
var import_value3 = require("@sinclair/typebox/value");
|
|
46
45
|
var import_ubiquity_os_logger3 = require("@ubiquity-os/ubiquity-os-logger");
|
|
47
46
|
|
|
47
|
+
// src/error.ts
|
|
48
|
+
var import_ubiquity_os_logger = require("@ubiquity-os/ubiquity-os-logger");
|
|
49
|
+
function getErrorStatus(err) {
|
|
50
|
+
if (!err || typeof err !== "object") return null;
|
|
51
|
+
const candidate = err;
|
|
52
|
+
const directStatus = candidate.status ?? candidate.response?.status;
|
|
53
|
+
if (typeof directStatus === "number" && Number.isFinite(directStatus)) return directStatus;
|
|
54
|
+
if (typeof directStatus === "string" && directStatus.trim()) {
|
|
55
|
+
const parsed = Number.parseInt(directStatus, 10);
|
|
56
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
57
|
+
}
|
|
58
|
+
if (err instanceof Error) {
|
|
59
|
+
const match = /LLM API error:\s*(\d{3})/i.exec(err.message);
|
|
60
|
+
if (match) {
|
|
61
|
+
const parsed = Number.parseInt(match[1], 10);
|
|
62
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function logByStatus(context, message, metadata) {
|
|
68
|
+
const status = getErrorStatus(metadata.err);
|
|
69
|
+
const payload = { ...metadata, ...status ? { status } : {} };
|
|
70
|
+
if (status && status >= 500) return context.logger.error(message, payload);
|
|
71
|
+
if (status && status >= 400) return context.logger.warn(message, payload);
|
|
72
|
+
if (status && status >= 300) return context.logger.debug(message, payload);
|
|
73
|
+
if (status && status >= 200) return context.logger.ok(message, payload);
|
|
74
|
+
if (status && status >= 100) return context.logger.info(message, payload);
|
|
75
|
+
return context.logger.error(message, payload);
|
|
76
|
+
}
|
|
77
|
+
function transformError(context, error) {
|
|
78
|
+
if (error instanceof import_ubiquity_os_logger.LogReturn) {
|
|
79
|
+
return error;
|
|
80
|
+
}
|
|
81
|
+
if (error instanceof AggregateError) {
|
|
82
|
+
const message = error.errors.map((err) => {
|
|
83
|
+
if (err instanceof import_ubiquity_os_logger.LogReturn) {
|
|
84
|
+
return err.logMessage.raw;
|
|
85
|
+
}
|
|
86
|
+
if (err instanceof Error) {
|
|
87
|
+
return err.message;
|
|
88
|
+
}
|
|
89
|
+
return String(err);
|
|
90
|
+
}).join("\n\n");
|
|
91
|
+
return logByStatus(context, message, { err: error });
|
|
92
|
+
}
|
|
93
|
+
if (error instanceof Error) {
|
|
94
|
+
return logByStatus(context, error.message, { err: error });
|
|
95
|
+
}
|
|
96
|
+
return logByStatus(context, String(error), { err: error });
|
|
97
|
+
}
|
|
98
|
+
|
|
48
99
|
// src/helpers/runtime-info.ts
|
|
49
|
-
var import_github = __toESM(require("@actions/github"));
|
|
50
100
|
var import_adapter = require("hono/adapter");
|
|
101
|
+
|
|
102
|
+
// src/helpers/github-context.ts
|
|
103
|
+
var github = __toESM(require("@actions/github"));
|
|
104
|
+
function getGithubContext() {
|
|
105
|
+
const override = globalThis.__UOS_GITHUB_CONTEXT__;
|
|
106
|
+
if (override) {
|
|
107
|
+
return override;
|
|
108
|
+
}
|
|
109
|
+
const module2 = github;
|
|
110
|
+
const context = module2.context ?? module2.default?.context;
|
|
111
|
+
if (!context) {
|
|
112
|
+
throw new Error("GitHub context is unavailable.");
|
|
113
|
+
}
|
|
114
|
+
return context;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/helpers/runtime-info.ts
|
|
51
118
|
var PluginRuntimeInfo = class _PluginRuntimeInfo {
|
|
52
119
|
static _instance = null;
|
|
53
120
|
_env = {};
|
|
@@ -95,10 +162,11 @@ var CfRuntimeInfo = class extends PluginRuntimeInfo {
|
|
|
95
162
|
};
|
|
96
163
|
var NodeRuntimeInfo = class extends PluginRuntimeInfo {
|
|
97
164
|
get version() {
|
|
98
|
-
return
|
|
165
|
+
return getGithubContext().sha;
|
|
99
166
|
}
|
|
100
167
|
get runUrl() {
|
|
101
|
-
|
|
168
|
+
const context = getGithubContext();
|
|
169
|
+
return context.payload.repository ? `${context.payload.repository?.html_url}/actions/runs/${context.runId}` : "http://localhost";
|
|
102
170
|
}
|
|
103
171
|
};
|
|
104
172
|
var DenoRuntimeInfo = class extends PluginRuntimeInfo {
|
|
@@ -139,7 +207,7 @@ var DenoRuntimeInfo = class extends PluginRuntimeInfo {
|
|
|
139
207
|
};
|
|
140
208
|
|
|
141
209
|
// src/util.ts
|
|
142
|
-
var
|
|
210
|
+
var import_ubiquity_os_logger2 = require("@ubiquity-os/ubiquity-os-logger");
|
|
143
211
|
|
|
144
212
|
// src/constants.ts
|
|
145
213
|
var KERNEL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
@@ -179,7 +247,7 @@ function getPluginOptions(options) {
|
|
|
179
247
|
return {
|
|
180
248
|
// Important to use || and not ?? to not consider empty strings
|
|
181
249
|
kernelPublicKey: options?.kernelPublicKey || KERNEL_PUBLIC_KEY,
|
|
182
|
-
logLevel: options?.logLevel ||
|
|
250
|
+
logLevel: options?.logLevel || import_ubiquity_os_logger2.LOG_LEVEL.INFO,
|
|
183
251
|
postCommentOnError: options?.postCommentOnError ?? true,
|
|
184
252
|
settingsSchema: options?.settingsSchema,
|
|
185
253
|
envSchema: options?.envSchema,
|
|
@@ -191,14 +259,75 @@ function getPluginOptions(options) {
|
|
|
191
259
|
}
|
|
192
260
|
|
|
193
261
|
// src/comment.ts
|
|
262
|
+
var COMMAND_RESPONSE_KIND = "command-response";
|
|
263
|
+
var COMMAND_RESPONSE_MARKER = `"commentKind": "${COMMAND_RESPONSE_KIND}"`;
|
|
264
|
+
var COMMAND_RESPONSE_COMMENT_LIMIT = 50;
|
|
265
|
+
var RECENT_COMMENTS_QUERY = (
|
|
266
|
+
/* GraphQL */
|
|
267
|
+
`
|
|
268
|
+
query ($owner: String!, $repo: String!, $number: Int!, $last: Int!) {
|
|
269
|
+
repository(owner: $owner, name: $repo) {
|
|
270
|
+
issueOrPullRequest(number: $number) {
|
|
271
|
+
__typename
|
|
272
|
+
... on Issue {
|
|
273
|
+
comments(last: $last) {
|
|
274
|
+
nodes {
|
|
275
|
+
id
|
|
276
|
+
body
|
|
277
|
+
isMinimized
|
|
278
|
+
minimizedReason
|
|
279
|
+
author {
|
|
280
|
+
login
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
... on PullRequest {
|
|
286
|
+
comments(last: $last) {
|
|
287
|
+
nodes {
|
|
288
|
+
id
|
|
289
|
+
body
|
|
290
|
+
isMinimized
|
|
291
|
+
minimizedReason
|
|
292
|
+
author {
|
|
293
|
+
login
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
`
|
|
302
|
+
);
|
|
303
|
+
var MINIMIZE_COMMENT_MUTATION = `
|
|
304
|
+
mutation($id: ID!, $classifier: ReportedContentClassifiers!) {
|
|
305
|
+
minimizeComment(input: { subjectId: $id, classifier: $classifier }) {
|
|
306
|
+
minimizedComment {
|
|
307
|
+
isMinimized
|
|
308
|
+
minimizedReason
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
`;
|
|
313
|
+
function logByStatus2(logger, message, status, metadata) {
|
|
314
|
+
const payload = { ...metadata, ...status ? { status } : {} };
|
|
315
|
+
if (status && status >= 500) return logger.error(message, payload);
|
|
316
|
+
if (status && status >= 400) return logger.warn(message, payload);
|
|
317
|
+
if (status && status >= 300) return logger.debug(message, payload);
|
|
318
|
+
if (status && status >= 200) return logger.ok(message, payload);
|
|
319
|
+
if (status && status >= 100) return logger.info(message, payload);
|
|
320
|
+
return logger.error(message, payload);
|
|
321
|
+
}
|
|
194
322
|
var CommentHandler = class _CommentHandler {
|
|
195
323
|
static HEADER_NAME = "UbiquityOS";
|
|
196
324
|
_lastCommentId = { reviewCommentId: null, issueCommentId: null };
|
|
197
|
-
|
|
325
|
+
_commandResponsePolicyApplied = false;
|
|
326
|
+
async _updateIssueComment(context, params) {
|
|
198
327
|
if (!this._lastCommentId.issueCommentId) {
|
|
199
|
-
throw
|
|
328
|
+
throw context.logger.error("issueCommentId is missing");
|
|
200
329
|
}
|
|
201
|
-
const commentData = await
|
|
330
|
+
const commentData = await context.octokit.rest.issues.updateComment({
|
|
202
331
|
owner: params.owner,
|
|
203
332
|
repo: params.repo,
|
|
204
333
|
comment_id: this._lastCommentId.issueCommentId,
|
|
@@ -206,11 +335,11 @@ var CommentHandler = class _CommentHandler {
|
|
|
206
335
|
});
|
|
207
336
|
return { ...commentData.data, issueNumber: params.issueNumber };
|
|
208
337
|
}
|
|
209
|
-
async _updateReviewComment(
|
|
338
|
+
async _updateReviewComment(context, params) {
|
|
210
339
|
if (!this._lastCommentId.reviewCommentId) {
|
|
211
|
-
throw
|
|
340
|
+
throw context.logger.error("reviewCommentId is missing");
|
|
212
341
|
}
|
|
213
|
-
const commentData = await
|
|
342
|
+
const commentData = await context.octokit.rest.pulls.updateReviewComment({
|
|
214
343
|
owner: params.owner,
|
|
215
344
|
repo: params.repo,
|
|
216
345
|
comment_id: this._lastCommentId.reviewCommentId,
|
|
@@ -218,9 +347,9 @@ var CommentHandler = class _CommentHandler {
|
|
|
218
347
|
});
|
|
219
348
|
return { ...commentData.data, issueNumber: params.issueNumber };
|
|
220
349
|
}
|
|
221
|
-
async _createNewComment(
|
|
350
|
+
async _createNewComment(context, params) {
|
|
222
351
|
if (params.commentId) {
|
|
223
|
-
const commentData2 = await
|
|
352
|
+
const commentData2 = await context.octokit.rest.pulls.createReplyForReviewComment({
|
|
224
353
|
owner: params.owner,
|
|
225
354
|
repo: params.repo,
|
|
226
355
|
pull_number: params.issueNumber,
|
|
@@ -230,7 +359,7 @@ var CommentHandler = class _CommentHandler {
|
|
|
230
359
|
this._lastCommentId.reviewCommentId = commentData2.data.id;
|
|
231
360
|
return { ...commentData2.data, issueNumber: params.issueNumber };
|
|
232
361
|
}
|
|
233
|
-
const commentData = await
|
|
362
|
+
const commentData = await context.octokit.rest.issues.createComment({
|
|
234
363
|
owner: params.owner,
|
|
235
364
|
repo: params.repo,
|
|
236
365
|
issue_number: params.issueNumber,
|
|
@@ -239,54 +368,142 @@ var CommentHandler = class _CommentHandler {
|
|
|
239
368
|
this._lastCommentId.issueCommentId = commentData.data.id;
|
|
240
369
|
return { ...commentData.data, issueNumber: params.issueNumber };
|
|
241
370
|
}
|
|
242
|
-
_getIssueNumber(
|
|
243
|
-
if ("issue" in
|
|
244
|
-
if ("pull_request" in
|
|
245
|
-
if ("discussion" in
|
|
371
|
+
_getIssueNumber(context) {
|
|
372
|
+
if ("issue" in context.payload) return context.payload.issue.number;
|
|
373
|
+
if ("pull_request" in context.payload) return context.payload.pull_request.number;
|
|
374
|
+
if ("discussion" in context.payload) return context.payload.discussion.number;
|
|
246
375
|
return void 0;
|
|
247
376
|
}
|
|
248
|
-
_getCommentId(
|
|
249
|
-
return "pull_request" in
|
|
377
|
+
_getCommentId(context) {
|
|
378
|
+
return "pull_request" in context.payload && "comment" in context.payload ? context.payload.comment.id : void 0;
|
|
379
|
+
}
|
|
380
|
+
_getCommentNodeId(context) {
|
|
381
|
+
const payload = context.payload;
|
|
382
|
+
const nodeId = payload.comment?.node_id;
|
|
383
|
+
return typeof nodeId === "string" && nodeId.trim() ? nodeId : null;
|
|
250
384
|
}
|
|
251
|
-
_extractIssueContext(
|
|
252
|
-
if (!("repository" in
|
|
385
|
+
_extractIssueContext(context) {
|
|
386
|
+
if (!("repository" in context.payload) || !context.payload.repository?.owner?.login) {
|
|
253
387
|
return null;
|
|
254
388
|
}
|
|
255
|
-
const issueNumber = this._getIssueNumber(
|
|
389
|
+
const issueNumber = this._getIssueNumber(context);
|
|
256
390
|
if (!issueNumber) return null;
|
|
257
391
|
return {
|
|
258
392
|
issueNumber,
|
|
259
|
-
commentId: this._getCommentId(
|
|
260
|
-
owner:
|
|
261
|
-
repo:
|
|
393
|
+
commentId: this._getCommentId(context),
|
|
394
|
+
owner: context.payload.repository.owner.login,
|
|
395
|
+
repo: context.payload.repository.name
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
_extractIssueLocator(context) {
|
|
399
|
+
if (!("issue" in context.payload) && !("pull_request" in context.payload)) {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
const issueContext = this._extractIssueContext(context);
|
|
403
|
+
if (!issueContext) return null;
|
|
404
|
+
return {
|
|
405
|
+
owner: issueContext.owner,
|
|
406
|
+
repo: issueContext.repo,
|
|
407
|
+
issueNumber: issueContext.issueNumber
|
|
262
408
|
};
|
|
263
409
|
}
|
|
264
|
-
|
|
410
|
+
_shouldApplyCommandResponsePolicy(context) {
|
|
411
|
+
return Boolean(context.command);
|
|
412
|
+
}
|
|
413
|
+
_isCommandResponseComment(body) {
|
|
414
|
+
return typeof body === "string" && body.includes(COMMAND_RESPONSE_MARKER);
|
|
415
|
+
}
|
|
416
|
+
_getGraphqlClient(context) {
|
|
417
|
+
const graphql = context.octokit.graphql;
|
|
418
|
+
return typeof graphql === "function" ? graphql : null;
|
|
419
|
+
}
|
|
420
|
+
async _fetchRecentComments(context, locator, last = COMMAND_RESPONSE_COMMENT_LIMIT) {
|
|
421
|
+
const graphql = this._getGraphqlClient(context);
|
|
422
|
+
if (!graphql) return [];
|
|
423
|
+
try {
|
|
424
|
+
const data = await graphql(RECENT_COMMENTS_QUERY, {
|
|
425
|
+
owner: locator.owner,
|
|
426
|
+
repo: locator.repo,
|
|
427
|
+
number: locator.issueNumber,
|
|
428
|
+
last
|
|
429
|
+
});
|
|
430
|
+
const nodes = data.repository?.issueOrPullRequest?.comments?.nodes ?? [];
|
|
431
|
+
return nodes.filter((node) => Boolean(node));
|
|
432
|
+
} catch (error) {
|
|
433
|
+
context.logger.debug("Failed to fetch recent comments (non-fatal)", { err: error });
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
_findPreviousCommandResponseComment(comments, currentCommentId) {
|
|
438
|
+
for (let idx = comments.length - 1; idx >= 0; idx -= 1) {
|
|
439
|
+
const comment = comments[idx];
|
|
440
|
+
if (!comment) continue;
|
|
441
|
+
if (currentCommentId && comment.id === currentCommentId) continue;
|
|
442
|
+
if (this._isCommandResponseComment(comment.body)) {
|
|
443
|
+
return comment;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
async _minimizeComment(context, commentNodeId, classifier = "RESOLVED") {
|
|
449
|
+
const graphql = this._getGraphqlClient(context);
|
|
450
|
+
if (!graphql) return;
|
|
451
|
+
try {
|
|
452
|
+
await graphql(MINIMIZE_COMMENT_MUTATION, {
|
|
453
|
+
id: commentNodeId,
|
|
454
|
+
classifier
|
|
455
|
+
});
|
|
456
|
+
} catch (error) {
|
|
457
|
+
context.logger.debug("Failed to minimize comment (non-fatal)", { err: error, commentNodeId });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async _applyCommandResponsePolicy(context) {
|
|
461
|
+
if (this._commandResponsePolicyApplied) return;
|
|
462
|
+
this._commandResponsePolicyApplied = true;
|
|
463
|
+
if (!this._shouldApplyCommandResponsePolicy(context)) return;
|
|
464
|
+
const locator = this._extractIssueLocator(context);
|
|
465
|
+
const commentNodeId = this._getCommentNodeId(context);
|
|
466
|
+
const comments = locator ? await this._fetchRecentComments(context, locator) : [];
|
|
467
|
+
const current = commentNodeId ? comments.find((comment) => comment.id === commentNodeId) : null;
|
|
468
|
+
const isCurrentMinimized = current?.isMinimized ?? false;
|
|
469
|
+
if (commentNodeId && !isCurrentMinimized) {
|
|
470
|
+
await this._minimizeComment(context, commentNodeId);
|
|
471
|
+
}
|
|
472
|
+
const previous = this._findPreviousCommandResponseComment(comments, commentNodeId);
|
|
473
|
+
if (previous && !previous.isMinimized) {
|
|
474
|
+
await this._minimizeComment(context, previous.id);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
_processMessage(context, message) {
|
|
265
478
|
if (message instanceof Error) {
|
|
266
479
|
const metadata2 = {
|
|
267
480
|
message: message.message,
|
|
268
481
|
name: message.name,
|
|
269
482
|
stack: message.stack
|
|
270
483
|
};
|
|
271
|
-
|
|
484
|
+
const status = getErrorStatus(message);
|
|
485
|
+
const logReturn = logByStatus2(context.logger, message.message, status, metadata2);
|
|
486
|
+
return { metadata: { ...metadata2, ...status ? { status } : {} }, logMessage: logReturn.logMessage };
|
|
272
487
|
}
|
|
488
|
+
const stackLine = message.metadata?.error?.stack?.split("\n")[2];
|
|
489
|
+
const callerMatch = stackLine ? /at (\S+)/.exec(stackLine) : null;
|
|
273
490
|
const metadata = message.metadata ? {
|
|
274
491
|
...message.metadata,
|
|
275
492
|
message: message.metadata.message,
|
|
276
493
|
stack: message.metadata.stack || message.metadata.error?.stack,
|
|
277
|
-
caller: message.metadata.caller ||
|
|
494
|
+
caller: message.metadata.caller || callerMatch?.[1]
|
|
278
495
|
} : { ...message };
|
|
279
496
|
return { metadata, logMessage: message.logMessage };
|
|
280
497
|
}
|
|
281
|
-
_getInstigatorName(
|
|
282
|
-
if ("installation" in
|
|
283
|
-
return
|
|
498
|
+
_getInstigatorName(context) {
|
|
499
|
+
if ("installation" in context.payload && context.payload.installation && "account" in context.payload.installation && context.payload.installation?.account?.name) {
|
|
500
|
+
return context.payload.installation?.account?.name;
|
|
284
501
|
}
|
|
285
|
-
return
|
|
502
|
+
return context.payload.sender?.login || _CommentHandler.HEADER_NAME;
|
|
286
503
|
}
|
|
287
|
-
_createMetadataContent(
|
|
504
|
+
_createMetadataContent(context, metadata) {
|
|
288
505
|
const jsonPretty = sanitizeMetadata(metadata);
|
|
289
|
-
const instigatorName = this._getInstigatorName(
|
|
506
|
+
const instigatorName = this._getInstigatorName(context);
|
|
290
507
|
const runUrl = PluginRuntimeInfo.getInstance().runUrl;
|
|
291
508
|
const version = PluginRuntimeInfo.getInstance().version;
|
|
292
509
|
const callingFnName = metadata.caller || "anonymous";
|
|
@@ -303,64 +520,46 @@ var CommentHandler = class _CommentHandler {
|
|
|
303
520
|
/*
|
|
304
521
|
* Creates the body for the comment, embeds the metadata and the header hidden in the body as well.
|
|
305
522
|
*/
|
|
306
|
-
createCommentBody(
|
|
307
|
-
return this._createCommentBody(
|
|
523
|
+
createCommentBody(context, message, options) {
|
|
524
|
+
return this._createCommentBody(context, message, options);
|
|
308
525
|
}
|
|
309
|
-
_createCommentBody(
|
|
310
|
-
const { metadata, logMessage } = this._processMessage(
|
|
311
|
-
const
|
|
526
|
+
_createCommentBody(context, message, options) {
|
|
527
|
+
const { metadata, logMessage } = this._processMessage(context, message);
|
|
528
|
+
const shouldTagCommandResponse = options?.commentKind && typeof metadata === "object" && !("commentKind" in metadata);
|
|
529
|
+
const metadataWithKind = shouldTagCommandResponse ? { ...metadata, commentKind: options?.commentKind } : metadata;
|
|
530
|
+
const { header, jsonPretty } = this._createMetadataContent(context, metadataWithKind);
|
|
312
531
|
const metadataContent = this._formatMetadataContent(logMessage, header, jsonPretty);
|
|
313
532
|
return `${options?.raw ? logMessage?.raw : logMessage?.diff}
|
|
314
533
|
|
|
315
534
|
${metadataContent}
|
|
316
535
|
`;
|
|
317
536
|
}
|
|
318
|
-
async postComment(
|
|
319
|
-
|
|
537
|
+
async postComment(context, message, options = { updateComment: true, raw: false }) {
|
|
538
|
+
await this._applyCommandResponsePolicy(context);
|
|
539
|
+
const issueContext = this._extractIssueContext(context);
|
|
320
540
|
if (!issueContext) {
|
|
321
|
-
|
|
541
|
+
context.logger.warn("Cannot post comment: missing issue context in payload");
|
|
322
542
|
return null;
|
|
323
543
|
}
|
|
324
|
-
const
|
|
544
|
+
const shouldTagCommandResponse = this._shouldApplyCommandResponsePolicy(context);
|
|
545
|
+
const body = this._createCommentBody(context, message, {
|
|
546
|
+
...options,
|
|
547
|
+
commentKind: options.commentKind ?? (shouldTagCommandResponse ? COMMAND_RESPONSE_KIND : void 0)
|
|
548
|
+
});
|
|
325
549
|
const { issueNumber, commentId, owner, repo } = issueContext;
|
|
326
550
|
const params = { owner, repo, body, issueNumber };
|
|
327
551
|
if (options.updateComment) {
|
|
328
|
-
if (this._lastCommentId.issueCommentId && !("pull_request" in
|
|
329
|
-
return this._updateIssueComment(
|
|
552
|
+
if (this._lastCommentId.issueCommentId && !("pull_request" in context.payload && "comment" in context.payload)) {
|
|
553
|
+
return this._updateIssueComment(context, params);
|
|
330
554
|
}
|
|
331
|
-
if (this._lastCommentId.reviewCommentId && "pull_request" in
|
|
332
|
-
return this._updateReviewComment(
|
|
555
|
+
if (this._lastCommentId.reviewCommentId && "pull_request" in context.payload && "comment" in context.payload) {
|
|
556
|
+
return this._updateReviewComment(context, params);
|
|
333
557
|
}
|
|
334
558
|
}
|
|
335
|
-
return this._createNewComment(
|
|
559
|
+
return this._createNewComment(context, { ...params, commentId });
|
|
336
560
|
}
|
|
337
561
|
};
|
|
338
562
|
|
|
339
|
-
// src/error.ts
|
|
340
|
-
var import_ubiquity_os_logger2 = require("@ubiquity-os/ubiquity-os-logger");
|
|
341
|
-
function transformError(context2, error) {
|
|
342
|
-
let loggerError;
|
|
343
|
-
if (error instanceof AggregateError) {
|
|
344
|
-
loggerError = context2.logger.error(
|
|
345
|
-
error.errors.map((err) => {
|
|
346
|
-
if (err instanceof import_ubiquity_os_logger2.LogReturn) {
|
|
347
|
-
return err.logMessage.raw;
|
|
348
|
-
} else if (err instanceof Error) {
|
|
349
|
-
return err.message;
|
|
350
|
-
} else {
|
|
351
|
-
return err;
|
|
352
|
-
}
|
|
353
|
-
}).join("\n\n"),
|
|
354
|
-
{ error }
|
|
355
|
-
);
|
|
356
|
-
} else if (error instanceof Error || error instanceof import_ubiquity_os_logger2.LogReturn) {
|
|
357
|
-
loggerError = error;
|
|
358
|
-
} else {
|
|
359
|
-
loggerError = context2.logger.error(String(error));
|
|
360
|
-
}
|
|
361
|
-
return loggerError;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
563
|
// src/helpers/command.ts
|
|
365
564
|
var import_value = require("@sinclair/typebox/value");
|
|
366
565
|
function getCommand(inputs, pluginOptions) {
|
|
@@ -431,7 +630,7 @@ async function verifySignature(publicKeyPem, inputs, signature) {
|
|
|
431
630
|
ref: inputs.ref,
|
|
432
631
|
command: inputs.command
|
|
433
632
|
};
|
|
434
|
-
const pemContents = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").
|
|
633
|
+
const pemContents = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replace(/\s+/g, "");
|
|
435
634
|
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
436
635
|
const publicKey = await crypto.subtle.importKey(
|
|
437
636
|
"spki",
|
|
@@ -483,90 +682,110 @@ var inputSchema = import_typebox3.Type.Object({
|
|
|
483
682
|
});
|
|
484
683
|
|
|
485
684
|
// src/actions.ts
|
|
486
|
-
async function handleError(
|
|
685
|
+
async function handleError(context, pluginOptions, error) {
|
|
487
686
|
console.error(error);
|
|
488
|
-
const loggerError = transformError(
|
|
489
|
-
|
|
490
|
-
core.setFailed(loggerError.logMessage.diff);
|
|
491
|
-
} else if (loggerError instanceof Error) {
|
|
492
|
-
core.setFailed(loggerError);
|
|
493
|
-
}
|
|
687
|
+
const loggerError = transformError(context, error);
|
|
688
|
+
core.setFailed(loggerError.logMessage.diff);
|
|
494
689
|
if (pluginOptions.postCommentOnError && loggerError) {
|
|
495
|
-
await
|
|
690
|
+
await context.commentHandler.postComment(context, loggerError);
|
|
496
691
|
}
|
|
497
692
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
if (!
|
|
693
|
+
function getDispatchTokenOrFail(pluginOptions) {
|
|
694
|
+
if (!pluginOptions.returnDataToKernel) return null;
|
|
695
|
+
const token = process.env.PLUGIN_GITHUB_TOKEN;
|
|
696
|
+
if (!token) {
|
|
502
697
|
core.setFailed("Error: PLUGIN_GITHUB_TOKEN env is not set");
|
|
503
|
-
return;
|
|
698
|
+
return null;
|
|
504
699
|
}
|
|
505
|
-
|
|
700
|
+
return token;
|
|
701
|
+
}
|
|
702
|
+
async function getInputsOrFail(pluginOptions) {
|
|
703
|
+
const githubContext = getGithubContext();
|
|
704
|
+
const body = githubContext.payload.inputs;
|
|
506
705
|
const inputSchemaErrors = [...import_value3.Value.Errors(inputSchema, body)];
|
|
507
706
|
if (inputSchemaErrors.length) {
|
|
508
707
|
console.dir(inputSchemaErrors, { depth: null });
|
|
509
708
|
core.setFailed(`Error: Invalid inputs payload: ${inputSchemaErrors.map((o) => o.message).join(", ")}`);
|
|
510
|
-
return;
|
|
709
|
+
return null;
|
|
511
710
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const inputs = import_value3.Value.Decode(inputSchema, body);
|
|
518
|
-
let config;
|
|
519
|
-
if (pluginOptions.settingsSchema) {
|
|
520
|
-
try {
|
|
521
|
-
config = import_value3.Value.Decode(pluginOptions.settingsSchema, import_value3.Value.Default(pluginOptions.settingsSchema, inputs.settings));
|
|
522
|
-
} catch (e) {
|
|
523
|
-
console.dir(...import_value3.Value.Errors(pluginOptions.settingsSchema, inputs.settings), { depth: null });
|
|
524
|
-
core.setFailed(`Error: Invalid settings provided.`);
|
|
525
|
-
throw e;
|
|
711
|
+
if (!pluginOptions.bypassSignatureVerification) {
|
|
712
|
+
const signature = typeof body.signature === "string" ? body.signature : "";
|
|
713
|
+
if (!signature) {
|
|
714
|
+
core.setFailed("Error: Missing signature");
|
|
715
|
+
return null;
|
|
526
716
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
if (pluginOptions.envSchema) {
|
|
532
|
-
try {
|
|
533
|
-
env = import_value3.Value.Decode(pluginOptions.envSchema, import_value3.Value.Default(pluginOptions.envSchema, process.env));
|
|
534
|
-
} catch (e) {
|
|
535
|
-
console.dir(...import_value3.Value.Errors(pluginOptions.envSchema, process.env), { depth: null });
|
|
536
|
-
core.setFailed(`Error: Invalid environment provided.`);
|
|
537
|
-
throw e;
|
|
717
|
+
const isValid = await verifySignature(pluginOptions.kernelPublicKey, body, signature);
|
|
718
|
+
if (!isValid) {
|
|
719
|
+
core.setFailed("Error: Invalid signature");
|
|
720
|
+
return null;
|
|
538
721
|
}
|
|
539
|
-
} else {
|
|
540
|
-
env = process.env;
|
|
541
722
|
}
|
|
542
|
-
|
|
543
|
-
|
|
723
|
+
return import_value3.Value.Decode(inputSchema, body);
|
|
724
|
+
}
|
|
725
|
+
function decodeWithSchema(schema, value, errorMessage) {
|
|
726
|
+
if (!schema) {
|
|
727
|
+
return { value };
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
return { value: import_value3.Value.Decode(schema, import_value3.Value.Default(schema, value)) };
|
|
731
|
+
} catch (error) {
|
|
732
|
+
console.dir(...import_value3.Value.Errors(schema, value), { depth: null });
|
|
733
|
+
const err = new Error(errorMessage);
|
|
734
|
+
err.cause = error;
|
|
735
|
+
return { value: null, error: err };
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
async function createActionsPlugin(handler, options) {
|
|
739
|
+
const pluginOptions = getPluginOptions(options);
|
|
740
|
+
const pluginGithubToken = getDispatchTokenOrFail(pluginOptions);
|
|
741
|
+
if (pluginOptions.returnDataToKernel && !pluginGithubToken) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const inputs = await getInputsOrFail(pluginOptions);
|
|
745
|
+
if (!inputs) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const context = {
|
|
544
749
|
eventName: inputs.eventName,
|
|
545
750
|
payload: inputs.eventPayload,
|
|
546
|
-
command,
|
|
751
|
+
command: null,
|
|
547
752
|
authToken: inputs.authToken,
|
|
548
753
|
ubiquityKernelToken: inputs.ubiquityKernelToken,
|
|
549
754
|
octokit: new customOctokit({ auth: inputs.authToken }),
|
|
550
|
-
config,
|
|
551
|
-
env,
|
|
755
|
+
config: inputs.settings,
|
|
756
|
+
env: process.env,
|
|
552
757
|
logger: new import_ubiquity_os_logger3.Logs(pluginOptions.logLevel),
|
|
553
758
|
commentHandler: new CommentHandler()
|
|
554
759
|
};
|
|
760
|
+
const configResult = decodeWithSchema(pluginOptions.settingsSchema, inputs.settings, "Error: Invalid settings provided.");
|
|
761
|
+
if (!configResult.value) {
|
|
762
|
+
await handleError(context, pluginOptions, configResult.error ?? new Error("Error: Invalid settings provided."));
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
context.config = configResult.value;
|
|
766
|
+
const envResult = decodeWithSchema(pluginOptions.envSchema, process.env, "Error: Invalid environment provided.");
|
|
767
|
+
if (!envResult.value) {
|
|
768
|
+
await handleError(context, pluginOptions, envResult.error ?? new Error("Error: Invalid environment provided."));
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
context.env = envResult.value;
|
|
555
772
|
try {
|
|
556
|
-
|
|
773
|
+
context.command = getCommand(inputs, pluginOptions);
|
|
774
|
+
const result = await handler(context);
|
|
557
775
|
core.setOutput("result", result);
|
|
558
|
-
if (pluginOptions
|
|
776
|
+
if (pluginOptions.returnDataToKernel && pluginGithubToken) {
|
|
559
777
|
await returnDataToKernel(pluginGithubToken, inputs.stateId, result);
|
|
560
778
|
}
|
|
561
779
|
} catch (error) {
|
|
562
|
-
await handleError(
|
|
780
|
+
await handleError(context, pluginOptions, error);
|
|
563
781
|
}
|
|
564
782
|
}
|
|
565
783
|
async function returnDataToKernel(repoToken, stateId, output) {
|
|
784
|
+
const githubContext = getGithubContext();
|
|
566
785
|
const octokit = new customOctokit({ auth: repoToken });
|
|
567
786
|
await octokit.rest.repos.createDispatchEvent({
|
|
568
|
-
owner:
|
|
569
|
-
repo:
|
|
787
|
+
owner: githubContext.repo.owner,
|
|
788
|
+
repo: githubContext.repo.repo,
|
|
570
789
|
event_type: "return-data-to-ubiquity-os-kernel",
|
|
571
790
|
client_payload: {
|
|
572
791
|
state_id: stateId,
|
|
@@ -628,107 +847,17 @@ function processSegment(segment, extraTags, shouldCollapseEmptyLines) {
|
|
|
628
847
|
return s;
|
|
629
848
|
}
|
|
630
849
|
|
|
631
|
-
// src/llm/index.ts
|
|
632
|
-
function normalizeBaseUrl(baseUrl) {
|
|
633
|
-
let normalized = baseUrl.trim();
|
|
634
|
-
while (normalized.endsWith("/")) {
|
|
635
|
-
normalized = normalized.slice(0, -1);
|
|
636
|
-
}
|
|
637
|
-
return normalized;
|
|
638
|
-
}
|
|
639
|
-
function getEnvString(name) {
|
|
640
|
-
if (typeof process === "undefined" || !process?.env) return "";
|
|
641
|
-
return String(process.env[name] ?? "").trim();
|
|
642
|
-
}
|
|
643
|
-
function getAiBaseUrl(options) {
|
|
644
|
-
if (typeof options.baseUrl === "string" && options.baseUrl.trim()) {
|
|
645
|
-
return normalizeBaseUrl(options.baseUrl);
|
|
646
|
-
}
|
|
647
|
-
const envBaseUrl = getEnvString("UBQ_AI_BASE_URL") || getEnvString("UBQ_AI_URL");
|
|
648
|
-
if (envBaseUrl) return normalizeBaseUrl(envBaseUrl);
|
|
649
|
-
return "https://ai.ubq.fi";
|
|
650
|
-
}
|
|
651
|
-
async function callLlm(options, input) {
|
|
652
|
-
const inputPayload = input;
|
|
653
|
-
const authToken = inputPayload.authToken;
|
|
654
|
-
const ubiquityKernelToken = inputPayload.ubiquityKernelToken;
|
|
655
|
-
const payload = inputPayload.payload ?? inputPayload.eventPayload;
|
|
656
|
-
const owner = payload?.repository?.owner?.login ?? "";
|
|
657
|
-
const repo = payload?.repository?.name ?? "";
|
|
658
|
-
const installationId = payload?.installation?.id;
|
|
659
|
-
if (!authToken) throw new Error("Missing authToken in inputs");
|
|
660
|
-
const isKernelTokenRequired = authToken.trim().startsWith("gh");
|
|
661
|
-
if (isKernelTokenRequired && !ubiquityKernelToken) {
|
|
662
|
-
throw new Error("Missing ubiquityKernelToken in inputs (kernel attestation is required for GitHub auth)");
|
|
663
|
-
}
|
|
664
|
-
const { baseUrl, model, stream: isStream, messages, ...rest } = options;
|
|
665
|
-
const url = `${getAiBaseUrl({ ...options, baseUrl })}/v1/chat/completions`;
|
|
666
|
-
const body = JSON.stringify({
|
|
667
|
-
...rest,
|
|
668
|
-
...model ? { model } : {},
|
|
669
|
-
messages,
|
|
670
|
-
stream: isStream ?? false
|
|
671
|
-
});
|
|
672
|
-
const headers = {
|
|
673
|
-
Authorization: `Bearer ${authToken}`,
|
|
674
|
-
"Content-Type": "application/json"
|
|
675
|
-
};
|
|
676
|
-
if (owner) headers["X-GitHub-Owner"] = owner;
|
|
677
|
-
if (repo) headers["X-GitHub-Repo"] = repo;
|
|
678
|
-
if (typeof installationId === "number" && Number.isFinite(installationId)) {
|
|
679
|
-
headers["X-GitHub-Installation-Id"] = String(installationId);
|
|
680
|
-
}
|
|
681
|
-
if (ubiquityKernelToken) {
|
|
682
|
-
headers["X-Ubiquity-Kernel-Token"] = ubiquityKernelToken;
|
|
683
|
-
}
|
|
684
|
-
const response = await fetch(url, { method: "POST", headers, body });
|
|
685
|
-
if (!response.ok) {
|
|
686
|
-
const err = await response.text();
|
|
687
|
-
throw new Error(`LLM API error: ${response.status} - ${err}`);
|
|
688
|
-
}
|
|
689
|
-
if (isStream) {
|
|
690
|
-
if (!response.body) {
|
|
691
|
-
throw new Error("LLM API error: missing response body for streaming request");
|
|
692
|
-
}
|
|
693
|
-
return parseSseStream(response.body);
|
|
694
|
-
}
|
|
695
|
-
return response.json();
|
|
696
|
-
}
|
|
697
|
-
async function* parseSseStream(body) {
|
|
698
|
-
const reader = body.getReader();
|
|
699
|
-
const decoder = new TextDecoder();
|
|
700
|
-
let buffer = "";
|
|
701
|
-
try {
|
|
702
|
-
while (true) {
|
|
703
|
-
const { value, done: isDone } = await reader.read();
|
|
704
|
-
if (isDone) break;
|
|
705
|
-
buffer += decoder.decode(value, { stream: true });
|
|
706
|
-
const events = buffer.split("\n\n");
|
|
707
|
-
buffer = events.pop() || "";
|
|
708
|
-
for (const event of events) {
|
|
709
|
-
if (event.startsWith("data: ")) {
|
|
710
|
-
const data = event.slice(6);
|
|
711
|
-
if (data === "[DONE]") return;
|
|
712
|
-
yield JSON.parse(data);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
} finally {
|
|
717
|
-
reader.releaseLock();
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
850
|
// src/server.ts
|
|
722
851
|
var import_value4 = require("@sinclair/typebox/value");
|
|
723
852
|
var import_ubiquity_os_logger4 = require("@ubiquity-os/ubiquity-os-logger");
|
|
724
853
|
var import_hono = require("hono");
|
|
725
854
|
var import_adapter2 = require("hono/adapter");
|
|
726
855
|
var import_http_exception = require("hono/http-exception");
|
|
727
|
-
async function handleError2(
|
|
856
|
+
async function handleError2(context, pluginOptions, error) {
|
|
728
857
|
console.error(error);
|
|
729
|
-
const loggerError = transformError(
|
|
858
|
+
const loggerError = transformError(context, error);
|
|
730
859
|
if (pluginOptions.postCommentOnError && loggerError) {
|
|
731
|
-
await
|
|
860
|
+
await context.commentHandler.postComment(context, loggerError);
|
|
732
861
|
}
|
|
733
862
|
throw new import_http_exception.HTTPException(500, { message: "Unexpected error" });
|
|
734
863
|
}
|
|
@@ -779,7 +908,7 @@ function createPlugin(handler, manifest, options) {
|
|
|
779
908
|
const workerName = new URL(inputs.ref).hostname.split(".")[0];
|
|
780
909
|
PluginRuntimeInfo.getInstance({ ...env, CLOUDFLARE_WORKER_NAME: workerName });
|
|
781
910
|
const command = getCommand(inputs, pluginOptions);
|
|
782
|
-
const
|
|
911
|
+
const context = {
|
|
783
912
|
eventName: inputs.eventName,
|
|
784
913
|
payload: inputs.eventPayload,
|
|
785
914
|
command,
|
|
@@ -792,14 +921,236 @@ function createPlugin(handler, manifest, options) {
|
|
|
792
921
|
commentHandler: new CommentHandler()
|
|
793
922
|
};
|
|
794
923
|
try {
|
|
795
|
-
const result = await handler(
|
|
924
|
+
const result = await handler(context);
|
|
796
925
|
return ctx.json({ stateId: inputs.stateId, output: result ?? {} });
|
|
797
926
|
} catch (error) {
|
|
798
|
-
await handleError2(
|
|
927
|
+
await handleError2(context, pluginOptions, error);
|
|
799
928
|
}
|
|
800
929
|
});
|
|
801
930
|
return app;
|
|
802
931
|
}
|
|
932
|
+
|
|
933
|
+
// src/llm/index.ts
|
|
934
|
+
var EMPTY_STRING = "";
|
|
935
|
+
function normalizeBaseUrl(baseUrl) {
|
|
936
|
+
let normalized = baseUrl.trim();
|
|
937
|
+
while (normalized.endsWith("/")) {
|
|
938
|
+
normalized = normalized.slice(0, -1);
|
|
939
|
+
}
|
|
940
|
+
return normalized;
|
|
941
|
+
}
|
|
942
|
+
var MAX_LLM_RETRIES = 2;
|
|
943
|
+
var RETRY_BACKOFF_MS = [250, 750];
|
|
944
|
+
function getRetryDelayMs(attempt) {
|
|
945
|
+
return RETRY_BACKOFF_MS[Math.min(attempt, RETRY_BACKOFF_MS.length - 1)] ?? 750;
|
|
946
|
+
}
|
|
947
|
+
function sleep(ms) {
|
|
948
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
949
|
+
}
|
|
950
|
+
function getEnvString(name) {
|
|
951
|
+
if (typeof process === "undefined" || !process?.env) return EMPTY_STRING;
|
|
952
|
+
return String(process.env[name] ?? EMPTY_STRING).trim();
|
|
953
|
+
}
|
|
954
|
+
function normalizeToken(value) {
|
|
955
|
+
return typeof value === "string" ? value.trim() : EMPTY_STRING;
|
|
956
|
+
}
|
|
957
|
+
function isGitHubToken(token) {
|
|
958
|
+
return token.trim().startsWith("gh");
|
|
959
|
+
}
|
|
960
|
+
function getEnvTokenFromInput(input) {
|
|
961
|
+
if ("env" in input) {
|
|
962
|
+
const envValue = input.env;
|
|
963
|
+
if (envValue && typeof envValue === "object") {
|
|
964
|
+
const token = normalizeToken(envValue.UOS_AI_TOKEN);
|
|
965
|
+
if (token) return token;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return getEnvString("UOS_AI_TOKEN");
|
|
969
|
+
}
|
|
970
|
+
function resolveAuthToken(input, aiAuthToken) {
|
|
971
|
+
const explicit = normalizeToken(aiAuthToken);
|
|
972
|
+
if (explicit) return { token: explicit, isGitHub: isGitHubToken(explicit) };
|
|
973
|
+
const envToken = getEnvTokenFromInput(input);
|
|
974
|
+
if (envToken) return { token: envToken, isGitHub: isGitHubToken(envToken) };
|
|
975
|
+
const fallback = normalizeToken(input.authToken);
|
|
976
|
+
if (!fallback) {
|
|
977
|
+
const err = new Error("Missing auth token; set UOS_AI_TOKEN, pass aiAuthToken, or provide authToken in input");
|
|
978
|
+
err.status = 401;
|
|
979
|
+
throw err;
|
|
980
|
+
}
|
|
981
|
+
return { token: fallback, isGitHub: isGitHubToken(fallback) };
|
|
982
|
+
}
|
|
983
|
+
function getAiBaseUrl(options) {
|
|
984
|
+
if (typeof options.baseUrl === "string" && options.baseUrl.trim()) {
|
|
985
|
+
return normalizeBaseUrl(options.baseUrl);
|
|
986
|
+
}
|
|
987
|
+
const envBaseUrl = getEnvString("UOS_AI_URL");
|
|
988
|
+
if (envBaseUrl) return normalizeBaseUrl(envBaseUrl);
|
|
989
|
+
return "https://ai.ubq.fi";
|
|
990
|
+
}
|
|
991
|
+
async function callLlm(options, input) {
|
|
992
|
+
const { baseUrl, model, stream: isStream, messages, aiAuthToken, ...rest } = options;
|
|
993
|
+
const { token: authToken, isGitHub } = resolveAuthToken(input, aiAuthToken);
|
|
994
|
+
const kernelToken = "ubiquityKernelToken" in input ? input.ubiquityKernelToken : void 0;
|
|
995
|
+
const payload = getPayload(input);
|
|
996
|
+
const { owner, repo, installationId } = getRepoMetadata(payload);
|
|
997
|
+
ensureMessages(messages);
|
|
998
|
+
const url = buildAiUrl(options, baseUrl);
|
|
999
|
+
const body = JSON.stringify({
|
|
1000
|
+
...rest,
|
|
1001
|
+
...model ? { model } : {},
|
|
1002
|
+
messages,
|
|
1003
|
+
stream: isStream ?? false
|
|
1004
|
+
});
|
|
1005
|
+
const headers = buildHeaders(authToken, {
|
|
1006
|
+
owner,
|
|
1007
|
+
repo,
|
|
1008
|
+
installationId,
|
|
1009
|
+
ubiquityKernelToken: isGitHub ? kernelToken : void 0
|
|
1010
|
+
});
|
|
1011
|
+
const response = await fetchWithRetry(url, { method: "POST", headers, body }, MAX_LLM_RETRIES);
|
|
1012
|
+
if (isStream) {
|
|
1013
|
+
if (!response.body) {
|
|
1014
|
+
throw new Error("LLM API error: missing response body for streaming request");
|
|
1015
|
+
}
|
|
1016
|
+
return parseSseStream(response.body);
|
|
1017
|
+
}
|
|
1018
|
+
const rawText = await response.text();
|
|
1019
|
+
try {
|
|
1020
|
+
return JSON.parse(rawText);
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
const preview = rawText ? rawText.slice(0, 1e3) : EMPTY_STRING;
|
|
1023
|
+
const message = "LLM API error: failed to parse JSON response from server" + (preview ? `; response body (truncated): ${preview}` : EMPTY_STRING);
|
|
1024
|
+
const error = new Error(message);
|
|
1025
|
+
error.cause = err;
|
|
1026
|
+
error.status = response.status;
|
|
1027
|
+
throw error;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
function ensureMessages(messages) {
|
|
1031
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
1032
|
+
const err = new Error("messages must be a non-empty array");
|
|
1033
|
+
err.status = 400;
|
|
1034
|
+
throw err;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function buildAiUrl(options, baseUrl) {
|
|
1038
|
+
return `${getAiBaseUrl({ ...options, baseUrl })}/v1/chat/completions`;
|
|
1039
|
+
}
|
|
1040
|
+
async function fetchWithRetry(url, options, maxRetries) {
|
|
1041
|
+
let attempt = 0;
|
|
1042
|
+
let lastError;
|
|
1043
|
+
while (attempt <= maxRetries) {
|
|
1044
|
+
try {
|
|
1045
|
+
const response = await fetch(url, options);
|
|
1046
|
+
if (response.ok) return response;
|
|
1047
|
+
throw await buildResponseError(response);
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
lastError = error;
|
|
1050
|
+
if (!shouldRetryError(error, attempt, maxRetries)) throw error;
|
|
1051
|
+
await sleep(getRetryDelayMs(attempt));
|
|
1052
|
+
attempt += 1;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
throw lastError ?? new Error("LLM API error: request failed after retries");
|
|
1056
|
+
}
|
|
1057
|
+
async function buildResponseError(response) {
|
|
1058
|
+
const errText = await response.text();
|
|
1059
|
+
const error = new Error(`LLM API error: ${response.status} - ${errText}`);
|
|
1060
|
+
error.status = response.status;
|
|
1061
|
+
return error;
|
|
1062
|
+
}
|
|
1063
|
+
function shouldRetryError(error, attempt, maxRetries) {
|
|
1064
|
+
if (attempt >= maxRetries) return false;
|
|
1065
|
+
const status = getErrorStatus2(error);
|
|
1066
|
+
if (typeof status === "number") {
|
|
1067
|
+
return status >= 500;
|
|
1068
|
+
}
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
function getErrorStatus2(error) {
|
|
1072
|
+
return typeof error?.status === "number" ? error.status : void 0;
|
|
1073
|
+
}
|
|
1074
|
+
function getPayload(input) {
|
|
1075
|
+
if ("payload" in input) {
|
|
1076
|
+
return input.payload;
|
|
1077
|
+
}
|
|
1078
|
+
return input.eventPayload;
|
|
1079
|
+
}
|
|
1080
|
+
function getRepoMetadata(payload) {
|
|
1081
|
+
const repoPayload = payload;
|
|
1082
|
+
return {
|
|
1083
|
+
owner: repoPayload?.repository?.owner?.login ?? EMPTY_STRING,
|
|
1084
|
+
repo: repoPayload?.repository?.name ?? EMPTY_STRING,
|
|
1085
|
+
installationId: repoPayload?.installation?.id
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
function buildHeaders(authToken, options) {
|
|
1089
|
+
const headers = {
|
|
1090
|
+
Authorization: `Bearer ${authToken}`,
|
|
1091
|
+
"Content-Type": "application/json"
|
|
1092
|
+
};
|
|
1093
|
+
if (options.owner) headers["X-GitHub-Owner"] = options.owner;
|
|
1094
|
+
if (options.repo) headers["X-GitHub-Repo"] = options.repo;
|
|
1095
|
+
if (typeof options.installationId === "number" && Number.isFinite(options.installationId)) {
|
|
1096
|
+
headers["X-GitHub-Installation-Id"] = String(options.installationId);
|
|
1097
|
+
}
|
|
1098
|
+
if (options.ubiquityKernelToken) {
|
|
1099
|
+
headers["X-Ubiquity-Kernel-Token"] = options.ubiquityKernelToken;
|
|
1100
|
+
}
|
|
1101
|
+
return headers;
|
|
1102
|
+
}
|
|
1103
|
+
async function* parseSseStream(body) {
|
|
1104
|
+
const reader = body.getReader();
|
|
1105
|
+
const decoder = new TextDecoder();
|
|
1106
|
+
let buffer = EMPTY_STRING;
|
|
1107
|
+
try {
|
|
1108
|
+
while (true) {
|
|
1109
|
+
const { value, done: isDone } = await reader.read();
|
|
1110
|
+
if (isDone) break;
|
|
1111
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1112
|
+
const { events, remainder } = splitSseEvents(buffer);
|
|
1113
|
+
buffer = remainder;
|
|
1114
|
+
for (const event of events) {
|
|
1115
|
+
const data = getEventData(event);
|
|
1116
|
+
if (!data) continue;
|
|
1117
|
+
if (data.trim() === "[DONE]") return;
|
|
1118
|
+
yield parseEventData(data);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
} finally {
|
|
1122
|
+
reader.releaseLock();
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
function splitSseEvents(buffer) {
|
|
1126
|
+
const normalized = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1127
|
+
const parts = normalized.split("\n\n");
|
|
1128
|
+
const remainder = parts.pop() ?? EMPTY_STRING;
|
|
1129
|
+
return { events: parts, remainder };
|
|
1130
|
+
}
|
|
1131
|
+
function getEventData(event) {
|
|
1132
|
+
if (!event.trim()) return null;
|
|
1133
|
+
const dataLines = event.split("\n").filter((line) => line.startsWith("data:"));
|
|
1134
|
+
if (!dataLines.length) return null;
|
|
1135
|
+
const data = dataLines.map((line) => line.startsWith("data: ") ? line.slice(6) : line.slice(5).replace(/^ /, EMPTY_STRING)).join("\n");
|
|
1136
|
+
return data || null;
|
|
1137
|
+
}
|
|
1138
|
+
function parseEventData(data) {
|
|
1139
|
+
try {
|
|
1140
|
+
return JSON.parse(data);
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
if (data.includes("\n")) {
|
|
1143
|
+
const collapsed = data.replace(/\n/g, EMPTY_STRING);
|
|
1144
|
+
try {
|
|
1145
|
+
return JSON.parse(collapsed);
|
|
1146
|
+
} catch {
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1150
|
+
const preview = data.length > 200 ? `${data.slice(0, 200)}...` : data;
|
|
1151
|
+
throw new Error(`LLM stream parse error: ${message}. Data: ${preview}`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
803
1154
|
// Annotate the CommonJS export names for ESM import in node:
|
|
804
1155
|
0 && (module.exports = {
|
|
805
1156
|
CommentHandler,
|