@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/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 import_github.default.context.sha;
165
+ return getGithubContext().sha;
99
166
  }
100
167
  get runUrl() {
101
- return import_github.default.context.payload.repository ? `${import_github.default.context.payload.repository?.html_url}/actions/runs/${import_github.default.context.runId}` : "http://localhost";
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 import_ubiquity_os_logger = require("@ubiquity-os/ubiquity-os-logger");
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 || import_ubiquity_os_logger.LOG_LEVEL.INFO,
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
- async _updateIssueComment(context2, params) {
325
+ _commandResponsePolicyApplied = false;
326
+ async _updateIssueComment(context, params) {
198
327
  if (!this._lastCommentId.issueCommentId) {
199
- throw context2.logger.error("issueCommentId is missing");
328
+ throw context.logger.error("issueCommentId is missing");
200
329
  }
201
- const commentData = await context2.octokit.rest.issues.updateComment({
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(context2, params) {
338
+ async _updateReviewComment(context, params) {
210
339
  if (!this._lastCommentId.reviewCommentId) {
211
- throw context2.logger.error("reviewCommentId is missing");
340
+ throw context.logger.error("reviewCommentId is missing");
212
341
  }
213
- const commentData = await context2.octokit.rest.pulls.updateReviewComment({
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(context2, params) {
350
+ async _createNewComment(context, params) {
222
351
  if (params.commentId) {
223
- const commentData2 = await context2.octokit.rest.pulls.createReplyForReviewComment({
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 context2.octokit.rest.issues.createComment({
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(context2) {
243
- if ("issue" in context2.payload) return context2.payload.issue.number;
244
- if ("pull_request" in context2.payload) return context2.payload.pull_request.number;
245
- if ("discussion" in context2.payload) return context2.payload.discussion.number;
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(context2) {
249
- return "pull_request" in context2.payload && "comment" in context2.payload ? context2.payload.comment.id : void 0;
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(context2) {
252
- if (!("repository" in context2.payload) || !context2.payload.repository?.owner?.login) {
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(context2);
389
+ const issueNumber = this._getIssueNumber(context);
256
390
  if (!issueNumber) return null;
257
391
  return {
258
392
  issueNumber,
259
- commentId: this._getCommentId(context2),
260
- owner: context2.payload.repository.owner.login,
261
- repo: context2.payload.repository.name
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
- _processMessage(context2, message) {
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
- return { metadata: metadata2, logMessage: context2.logger.error(message.message).logMessage };
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 || message.metadata.error?.stack?.split("\n")[2]?.match(/at (\S+)/)?.[1]
494
+ caller: message.metadata.caller || callerMatch?.[1]
278
495
  } : { ...message };
279
496
  return { metadata, logMessage: message.logMessage };
280
497
  }
281
- _getInstigatorName(context2) {
282
- if ("installation" in context2.payload && context2.payload.installation && "account" in context2.payload.installation && context2.payload.installation?.account?.name) {
283
- return context2.payload.installation?.account?.name;
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 context2.payload.sender?.login || _CommentHandler.HEADER_NAME;
502
+ return context.payload.sender?.login || _CommentHandler.HEADER_NAME;
286
503
  }
287
- _createMetadataContent(context2, metadata) {
504
+ _createMetadataContent(context, metadata) {
288
505
  const jsonPretty = sanitizeMetadata(metadata);
289
- const instigatorName = this._getInstigatorName(context2);
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(context2, message, options) {
307
- return this._createCommentBody(context2, message, options);
523
+ createCommentBody(context, message, options) {
524
+ return this._createCommentBody(context, message, options);
308
525
  }
309
- _createCommentBody(context2, message, options) {
310
- const { metadata, logMessage } = this._processMessage(context2, message);
311
- const { header, jsonPretty } = this._createMetadataContent(context2, metadata);
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(context2, message, options = { updateComment: true, raw: false }) {
319
- const issueContext = this._extractIssueContext(context2);
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
- context2.logger.info("Cannot post comment: missing issue context in payload");
541
+ context.logger.warn("Cannot post comment: missing issue context in payload");
322
542
  return null;
323
543
  }
324
- const body = this._createCommentBody(context2, message, options);
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 context2.payload && "comment" in context2.payload)) {
329
- return this._updateIssueComment(context2, params);
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 context2.payload && "comment" in context2.payload) {
332
- return this._updateReviewComment(context2, params);
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(context2, { ...params, commentId });
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-----", "").trim();
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(context2, pluginOptions, error) {
685
+ async function handleError(context, pluginOptions, error) {
487
686
  console.error(error);
488
- const loggerError = transformError(context2, error);
489
- if (loggerError instanceof import_ubiquity_os_logger3.LogReturn) {
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 context2.commentHandler.postComment(context2, loggerError);
690
+ await context.commentHandler.postComment(context, loggerError);
496
691
  }
497
692
  }
498
- async function createActionsPlugin(handler, options) {
499
- const pluginOptions = getPluginOptions(options);
500
- const pluginGithubToken = process.env.PLUGIN_GITHUB_TOKEN;
501
- if (!pluginGithubToken) {
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
- const body = github2.context.payload.inputs;
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
- const signature = body.signature;
513
- if (!pluginOptions.bypassSignatureVerification && !await verifySignature(pluginOptions.kernelPublicKey, body, signature)) {
514
- core.setFailed(`Error: Invalid signature`);
515
- return;
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
- } else {
528
- config = inputs.settings;
529
- }
530
- let env;
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
- const command = getCommand(inputs, pluginOptions);
543
- const context2 = {
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
- const result = await handler(context2);
773
+ context.command = getCommand(inputs, pluginOptions);
774
+ const result = await handler(context);
557
775
  core.setOutput("result", result);
558
- if (pluginOptions?.returnDataToKernel) {
776
+ if (pluginOptions.returnDataToKernel && pluginGithubToken) {
559
777
  await returnDataToKernel(pluginGithubToken, inputs.stateId, result);
560
778
  }
561
779
  } catch (error) {
562
- await handleError(context2, pluginOptions, error);
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: github2.context.repo.owner,
569
- repo: github2.context.repo.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(context2, pluginOptions, error) {
856
+ async function handleError2(context, pluginOptions, error) {
728
857
  console.error(error);
729
- const loggerError = transformError(context2, error);
858
+ const loggerError = transformError(context, error);
730
859
  if (pluginOptions.postCommentOnError && loggerError) {
731
- await context2.commentHandler.postComment(context2, loggerError);
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 context2 = {
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(context2);
924
+ const result = await handler(context);
796
925
  return ctx.json({ stateId: inputs.stateId, output: result ?? {} });
797
926
  } catch (error) {
798
- await handleError2(context2, pluginOptions, error);
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,