@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.mjs CHANGED
@@ -1,12 +1,79 @@
1
1
  // src/actions.ts
2
2
  import * as core from "@actions/core";
3
- import * as github2 from "@actions/github";
4
3
  import { Value as Value3 } from "@sinclair/typebox/value";
5
- import { LogReturn as LogReturn3, Logs } from "@ubiquity-os/ubiquity-os-logger";
4
+ import { Logs } from "@ubiquity-os/ubiquity-os-logger";
5
+
6
+ // src/error.ts
7
+ import { LogReturn } from "@ubiquity-os/ubiquity-os-logger";
8
+ function getErrorStatus(err) {
9
+ if (!err || typeof err !== "object") return null;
10
+ const candidate = err;
11
+ const directStatus = candidate.status ?? candidate.response?.status;
12
+ if (typeof directStatus === "number" && Number.isFinite(directStatus)) return directStatus;
13
+ if (typeof directStatus === "string" && directStatus.trim()) {
14
+ const parsed = Number.parseInt(directStatus, 10);
15
+ if (Number.isFinite(parsed)) return parsed;
16
+ }
17
+ if (err instanceof Error) {
18
+ const match = /LLM API error:\s*(\d{3})/i.exec(err.message);
19
+ if (match) {
20
+ const parsed = Number.parseInt(match[1], 10);
21
+ if (Number.isFinite(parsed)) return parsed;
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ function logByStatus(context, message, metadata) {
27
+ const status = getErrorStatus(metadata.err);
28
+ const payload = { ...metadata, ...status ? { status } : {} };
29
+ if (status && status >= 500) return context.logger.error(message, payload);
30
+ if (status && status >= 400) return context.logger.warn(message, payload);
31
+ if (status && status >= 300) return context.logger.debug(message, payload);
32
+ if (status && status >= 200) return context.logger.ok(message, payload);
33
+ if (status && status >= 100) return context.logger.info(message, payload);
34
+ return context.logger.error(message, payload);
35
+ }
36
+ function transformError(context, error) {
37
+ if (error instanceof LogReturn) {
38
+ return error;
39
+ }
40
+ if (error instanceof AggregateError) {
41
+ const message = error.errors.map((err) => {
42
+ if (err instanceof LogReturn) {
43
+ return err.logMessage.raw;
44
+ }
45
+ if (err instanceof Error) {
46
+ return err.message;
47
+ }
48
+ return String(err);
49
+ }).join("\n\n");
50
+ return logByStatus(context, message, { err: error });
51
+ }
52
+ if (error instanceof Error) {
53
+ return logByStatus(context, error.message, { err: error });
54
+ }
55
+ return logByStatus(context, String(error), { err: error });
56
+ }
6
57
 
7
58
  // src/helpers/runtime-info.ts
8
- import github from "@actions/github";
9
59
  import { getRuntimeKey } from "hono/adapter";
60
+
61
+ // src/helpers/github-context.ts
62
+ import * as github from "@actions/github";
63
+ function getGithubContext() {
64
+ const override = globalThis.__UOS_GITHUB_CONTEXT__;
65
+ if (override) {
66
+ return override;
67
+ }
68
+ const module = github;
69
+ const context = module.context ?? module.default?.context;
70
+ if (!context) {
71
+ throw new Error("GitHub context is unavailable.");
72
+ }
73
+ return context;
74
+ }
75
+
76
+ // src/helpers/runtime-info.ts
10
77
  var PluginRuntimeInfo = class _PluginRuntimeInfo {
11
78
  static _instance = null;
12
79
  _env = {};
@@ -54,10 +121,11 @@ var CfRuntimeInfo = class extends PluginRuntimeInfo {
54
121
  };
55
122
  var NodeRuntimeInfo = class extends PluginRuntimeInfo {
56
123
  get version() {
57
- return github.context.sha;
124
+ return getGithubContext().sha;
58
125
  }
59
126
  get runUrl() {
60
- return github.context.payload.repository ? `${github.context.payload.repository?.html_url}/actions/runs/${github.context.runId}` : "http://localhost";
127
+ const context = getGithubContext();
128
+ return context.payload.repository ? `${context.payload.repository?.html_url}/actions/runs/${context.runId}` : "http://localhost";
61
129
  }
62
130
  };
63
131
  var DenoRuntimeInfo = class extends PluginRuntimeInfo {
@@ -150,14 +218,75 @@ function getPluginOptions(options) {
150
218
  }
151
219
 
152
220
  // src/comment.ts
221
+ var COMMAND_RESPONSE_KIND = "command-response";
222
+ var COMMAND_RESPONSE_MARKER = `"commentKind": "${COMMAND_RESPONSE_KIND}"`;
223
+ var COMMAND_RESPONSE_COMMENT_LIMIT = 50;
224
+ var RECENT_COMMENTS_QUERY = (
225
+ /* GraphQL */
226
+ `
227
+ query ($owner: String!, $repo: String!, $number: Int!, $last: Int!) {
228
+ repository(owner: $owner, name: $repo) {
229
+ issueOrPullRequest(number: $number) {
230
+ __typename
231
+ ... on Issue {
232
+ comments(last: $last) {
233
+ nodes {
234
+ id
235
+ body
236
+ isMinimized
237
+ minimizedReason
238
+ author {
239
+ login
240
+ }
241
+ }
242
+ }
243
+ }
244
+ ... on PullRequest {
245
+ comments(last: $last) {
246
+ nodes {
247
+ id
248
+ body
249
+ isMinimized
250
+ minimizedReason
251
+ author {
252
+ login
253
+ }
254
+ }
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
260
+ `
261
+ );
262
+ var MINIMIZE_COMMENT_MUTATION = `
263
+ mutation($id: ID!, $classifier: ReportedContentClassifiers!) {
264
+ minimizeComment(input: { subjectId: $id, classifier: $classifier }) {
265
+ minimizedComment {
266
+ isMinimized
267
+ minimizedReason
268
+ }
269
+ }
270
+ }
271
+ `;
272
+ function logByStatus2(logger, message, status, metadata) {
273
+ const payload = { ...metadata, ...status ? { status } : {} };
274
+ if (status && status >= 500) return logger.error(message, payload);
275
+ if (status && status >= 400) return logger.warn(message, payload);
276
+ if (status && status >= 300) return logger.debug(message, payload);
277
+ if (status && status >= 200) return logger.ok(message, payload);
278
+ if (status && status >= 100) return logger.info(message, payload);
279
+ return logger.error(message, payload);
280
+ }
153
281
  var CommentHandler = class _CommentHandler {
154
282
  static HEADER_NAME = "UbiquityOS";
155
283
  _lastCommentId = { reviewCommentId: null, issueCommentId: null };
156
- async _updateIssueComment(context2, params) {
284
+ _commandResponsePolicyApplied = false;
285
+ async _updateIssueComment(context, params) {
157
286
  if (!this._lastCommentId.issueCommentId) {
158
- throw context2.logger.error("issueCommentId is missing");
287
+ throw context.logger.error("issueCommentId is missing");
159
288
  }
160
- const commentData = await context2.octokit.rest.issues.updateComment({
289
+ const commentData = await context.octokit.rest.issues.updateComment({
161
290
  owner: params.owner,
162
291
  repo: params.repo,
163
292
  comment_id: this._lastCommentId.issueCommentId,
@@ -165,11 +294,11 @@ var CommentHandler = class _CommentHandler {
165
294
  });
166
295
  return { ...commentData.data, issueNumber: params.issueNumber };
167
296
  }
168
- async _updateReviewComment(context2, params) {
297
+ async _updateReviewComment(context, params) {
169
298
  if (!this._lastCommentId.reviewCommentId) {
170
- throw context2.logger.error("reviewCommentId is missing");
299
+ throw context.logger.error("reviewCommentId is missing");
171
300
  }
172
- const commentData = await context2.octokit.rest.pulls.updateReviewComment({
301
+ const commentData = await context.octokit.rest.pulls.updateReviewComment({
173
302
  owner: params.owner,
174
303
  repo: params.repo,
175
304
  comment_id: this._lastCommentId.reviewCommentId,
@@ -177,9 +306,9 @@ var CommentHandler = class _CommentHandler {
177
306
  });
178
307
  return { ...commentData.data, issueNumber: params.issueNumber };
179
308
  }
180
- async _createNewComment(context2, params) {
309
+ async _createNewComment(context, params) {
181
310
  if (params.commentId) {
182
- const commentData2 = await context2.octokit.rest.pulls.createReplyForReviewComment({
311
+ const commentData2 = await context.octokit.rest.pulls.createReplyForReviewComment({
183
312
  owner: params.owner,
184
313
  repo: params.repo,
185
314
  pull_number: params.issueNumber,
@@ -189,7 +318,7 @@ var CommentHandler = class _CommentHandler {
189
318
  this._lastCommentId.reviewCommentId = commentData2.data.id;
190
319
  return { ...commentData2.data, issueNumber: params.issueNumber };
191
320
  }
192
- const commentData = await context2.octokit.rest.issues.createComment({
321
+ const commentData = await context.octokit.rest.issues.createComment({
193
322
  owner: params.owner,
194
323
  repo: params.repo,
195
324
  issue_number: params.issueNumber,
@@ -198,54 +327,142 @@ var CommentHandler = class _CommentHandler {
198
327
  this._lastCommentId.issueCommentId = commentData.data.id;
199
328
  return { ...commentData.data, issueNumber: params.issueNumber };
200
329
  }
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;
330
+ _getIssueNumber(context) {
331
+ if ("issue" in context.payload) return context.payload.issue.number;
332
+ if ("pull_request" in context.payload) return context.payload.pull_request.number;
333
+ if ("discussion" in context.payload) return context.payload.discussion.number;
205
334
  return void 0;
206
335
  }
207
- _getCommentId(context2) {
208
- return "pull_request" in context2.payload && "comment" in context2.payload ? context2.payload.comment.id : void 0;
336
+ _getCommentId(context) {
337
+ return "pull_request" in context.payload && "comment" in context.payload ? context.payload.comment.id : void 0;
338
+ }
339
+ _getCommentNodeId(context) {
340
+ const payload = context.payload;
341
+ const nodeId = payload.comment?.node_id;
342
+ return typeof nodeId === "string" && nodeId.trim() ? nodeId : null;
209
343
  }
210
- _extractIssueContext(context2) {
211
- if (!("repository" in context2.payload) || !context2.payload.repository?.owner?.login) {
344
+ _extractIssueContext(context) {
345
+ if (!("repository" in context.payload) || !context.payload.repository?.owner?.login) {
212
346
  return null;
213
347
  }
214
- const issueNumber = this._getIssueNumber(context2);
348
+ const issueNumber = this._getIssueNumber(context);
215
349
  if (!issueNumber) return null;
216
350
  return {
217
351
  issueNumber,
218
- commentId: this._getCommentId(context2),
219
- owner: context2.payload.repository.owner.login,
220
- repo: context2.payload.repository.name
352
+ commentId: this._getCommentId(context),
353
+ owner: context.payload.repository.owner.login,
354
+ repo: context.payload.repository.name
355
+ };
356
+ }
357
+ _extractIssueLocator(context) {
358
+ if (!("issue" in context.payload) && !("pull_request" in context.payload)) {
359
+ return null;
360
+ }
361
+ const issueContext = this._extractIssueContext(context);
362
+ if (!issueContext) return null;
363
+ return {
364
+ owner: issueContext.owner,
365
+ repo: issueContext.repo,
366
+ issueNumber: issueContext.issueNumber
221
367
  };
222
368
  }
223
- _processMessage(context2, message) {
369
+ _shouldApplyCommandResponsePolicy(context) {
370
+ return Boolean(context.command);
371
+ }
372
+ _isCommandResponseComment(body) {
373
+ return typeof body === "string" && body.includes(COMMAND_RESPONSE_MARKER);
374
+ }
375
+ _getGraphqlClient(context) {
376
+ const graphql = context.octokit.graphql;
377
+ return typeof graphql === "function" ? graphql : null;
378
+ }
379
+ async _fetchRecentComments(context, locator, last = COMMAND_RESPONSE_COMMENT_LIMIT) {
380
+ const graphql = this._getGraphqlClient(context);
381
+ if (!graphql) return [];
382
+ try {
383
+ const data = await graphql(RECENT_COMMENTS_QUERY, {
384
+ owner: locator.owner,
385
+ repo: locator.repo,
386
+ number: locator.issueNumber,
387
+ last
388
+ });
389
+ const nodes = data.repository?.issueOrPullRequest?.comments?.nodes ?? [];
390
+ return nodes.filter((node) => Boolean(node));
391
+ } catch (error) {
392
+ context.logger.debug("Failed to fetch recent comments (non-fatal)", { err: error });
393
+ return [];
394
+ }
395
+ }
396
+ _findPreviousCommandResponseComment(comments, currentCommentId) {
397
+ for (let idx = comments.length - 1; idx >= 0; idx -= 1) {
398
+ const comment = comments[idx];
399
+ if (!comment) continue;
400
+ if (currentCommentId && comment.id === currentCommentId) continue;
401
+ if (this._isCommandResponseComment(comment.body)) {
402
+ return comment;
403
+ }
404
+ }
405
+ return null;
406
+ }
407
+ async _minimizeComment(context, commentNodeId, classifier = "RESOLVED") {
408
+ const graphql = this._getGraphqlClient(context);
409
+ if (!graphql) return;
410
+ try {
411
+ await graphql(MINIMIZE_COMMENT_MUTATION, {
412
+ id: commentNodeId,
413
+ classifier
414
+ });
415
+ } catch (error) {
416
+ context.logger.debug("Failed to minimize comment (non-fatal)", { err: error, commentNodeId });
417
+ }
418
+ }
419
+ async _applyCommandResponsePolicy(context) {
420
+ if (this._commandResponsePolicyApplied) return;
421
+ this._commandResponsePolicyApplied = true;
422
+ if (!this._shouldApplyCommandResponsePolicy(context)) return;
423
+ const locator = this._extractIssueLocator(context);
424
+ const commentNodeId = this._getCommentNodeId(context);
425
+ const comments = locator ? await this._fetchRecentComments(context, locator) : [];
426
+ const current = commentNodeId ? comments.find((comment) => comment.id === commentNodeId) : null;
427
+ const isCurrentMinimized = current?.isMinimized ?? false;
428
+ if (commentNodeId && !isCurrentMinimized) {
429
+ await this._minimizeComment(context, commentNodeId);
430
+ }
431
+ const previous = this._findPreviousCommandResponseComment(comments, commentNodeId);
432
+ if (previous && !previous.isMinimized) {
433
+ await this._minimizeComment(context, previous.id);
434
+ }
435
+ }
436
+ _processMessage(context, message) {
224
437
  if (message instanceof Error) {
225
438
  const metadata2 = {
226
439
  message: message.message,
227
440
  name: message.name,
228
441
  stack: message.stack
229
442
  };
230
- return { metadata: metadata2, logMessage: context2.logger.error(message.message).logMessage };
443
+ const status = getErrorStatus(message);
444
+ const logReturn = logByStatus2(context.logger, message.message, status, metadata2);
445
+ return { metadata: { ...metadata2, ...status ? { status } : {} }, logMessage: logReturn.logMessage };
231
446
  }
447
+ const stackLine = message.metadata?.error?.stack?.split("\n")[2];
448
+ const callerMatch = stackLine ? /at (\S+)/.exec(stackLine) : null;
232
449
  const metadata = message.metadata ? {
233
450
  ...message.metadata,
234
451
  message: message.metadata.message,
235
452
  stack: message.metadata.stack || message.metadata.error?.stack,
236
- caller: message.metadata.caller || message.metadata.error?.stack?.split("\n")[2]?.match(/at (\S+)/)?.[1]
453
+ caller: message.metadata.caller || callerMatch?.[1]
237
454
  } : { ...message };
238
455
  return { metadata, logMessage: message.logMessage };
239
456
  }
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;
457
+ _getInstigatorName(context) {
458
+ if ("installation" in context.payload && context.payload.installation && "account" in context.payload.installation && context.payload.installation?.account?.name) {
459
+ return context.payload.installation?.account?.name;
243
460
  }
244
- return context2.payload.sender?.login || _CommentHandler.HEADER_NAME;
461
+ return context.payload.sender?.login || _CommentHandler.HEADER_NAME;
245
462
  }
246
- _createMetadataContent(context2, metadata) {
463
+ _createMetadataContent(context, metadata) {
247
464
  const jsonPretty = sanitizeMetadata(metadata);
248
- const instigatorName = this._getInstigatorName(context2);
465
+ const instigatorName = this._getInstigatorName(context);
249
466
  const runUrl = PluginRuntimeInfo.getInstance().runUrl;
250
467
  const version = PluginRuntimeInfo.getInstance().version;
251
468
  const callingFnName = metadata.caller || "anonymous";
@@ -262,64 +479,46 @@ var CommentHandler = class _CommentHandler {
262
479
  /*
263
480
  * Creates the body for the comment, embeds the metadata and the header hidden in the body as well.
264
481
  */
265
- createCommentBody(context2, message, options) {
266
- return this._createCommentBody(context2, message, options);
482
+ createCommentBody(context, message, options) {
483
+ return this._createCommentBody(context, message, options);
267
484
  }
268
- _createCommentBody(context2, message, options) {
269
- const { metadata, logMessage } = this._processMessage(context2, message);
270
- const { header, jsonPretty } = this._createMetadataContent(context2, metadata);
485
+ _createCommentBody(context, message, options) {
486
+ const { metadata, logMessage } = this._processMessage(context, message);
487
+ const shouldTagCommandResponse = options?.commentKind && typeof metadata === "object" && !("commentKind" in metadata);
488
+ const metadataWithKind = shouldTagCommandResponse ? { ...metadata, commentKind: options?.commentKind } : metadata;
489
+ const { header, jsonPretty } = this._createMetadataContent(context, metadataWithKind);
271
490
  const metadataContent = this._formatMetadataContent(logMessage, header, jsonPretty);
272
491
  return `${options?.raw ? logMessage?.raw : logMessage?.diff}
273
492
 
274
493
  ${metadataContent}
275
494
  `;
276
495
  }
277
- async postComment(context2, message, options = { updateComment: true, raw: false }) {
278
- const issueContext = this._extractIssueContext(context2);
496
+ async postComment(context, message, options = { updateComment: true, raw: false }) {
497
+ await this._applyCommandResponsePolicy(context);
498
+ const issueContext = this._extractIssueContext(context);
279
499
  if (!issueContext) {
280
- context2.logger.info("Cannot post comment: missing issue context in payload");
500
+ context.logger.warn("Cannot post comment: missing issue context in payload");
281
501
  return null;
282
502
  }
283
- const body = this._createCommentBody(context2, message, options);
503
+ const shouldTagCommandResponse = this._shouldApplyCommandResponsePolicy(context);
504
+ const body = this._createCommentBody(context, message, {
505
+ ...options,
506
+ commentKind: options.commentKind ?? (shouldTagCommandResponse ? COMMAND_RESPONSE_KIND : void 0)
507
+ });
284
508
  const { issueNumber, commentId, owner, repo } = issueContext;
285
509
  const params = { owner, repo, body, issueNumber };
286
510
  if (options.updateComment) {
287
- if (this._lastCommentId.issueCommentId && !("pull_request" in context2.payload && "comment" in context2.payload)) {
288
- return this._updateIssueComment(context2, params);
511
+ if (this._lastCommentId.issueCommentId && !("pull_request" in context.payload && "comment" in context.payload)) {
512
+ return this._updateIssueComment(context, params);
289
513
  }
290
- if (this._lastCommentId.reviewCommentId && "pull_request" in context2.payload && "comment" in context2.payload) {
291
- return this._updateReviewComment(context2, params);
514
+ if (this._lastCommentId.reviewCommentId && "pull_request" in context.payload && "comment" in context.payload) {
515
+ return this._updateReviewComment(context, params);
292
516
  }
293
517
  }
294
- return this._createNewComment(context2, { ...params, commentId });
518
+ return this._createNewComment(context, { ...params, commentId });
295
519
  }
296
520
  };
297
521
 
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
-
323
522
  // src/helpers/command.ts
324
523
  import { Value } from "@sinclair/typebox/value";
325
524
  function getCommand(inputs, pluginOptions) {
@@ -390,7 +589,7 @@ async function verifySignature(publicKeyPem, inputs, signature) {
390
589
  ref: inputs.ref,
391
590
  command: inputs.command
392
591
  };
393
- const pemContents = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").trim();
592
+ const pemContents = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replace(/\s+/g, "");
394
593
  const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
395
594
  const publicKey = await crypto.subtle.importKey(
396
595
  "spki",
@@ -442,90 +641,110 @@ var inputSchema = T2.Object({
442
641
  });
443
642
 
444
643
  // src/actions.ts
445
- async function handleError(context2, pluginOptions, error) {
644
+ async function handleError(context, pluginOptions, error) {
446
645
  console.error(error);
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
- }
646
+ const loggerError = transformError(context, error);
647
+ core.setFailed(loggerError.logMessage.diff);
453
648
  if (pluginOptions.postCommentOnError && loggerError) {
454
- await context2.commentHandler.postComment(context2, loggerError);
649
+ await context.commentHandler.postComment(context, loggerError);
455
650
  }
456
651
  }
457
- async function createActionsPlugin(handler, options) {
458
- const pluginOptions = getPluginOptions(options);
459
- const pluginGithubToken = process.env.PLUGIN_GITHUB_TOKEN;
460
- if (!pluginGithubToken) {
652
+ function getDispatchTokenOrFail(pluginOptions) {
653
+ if (!pluginOptions.returnDataToKernel) return null;
654
+ const token = process.env.PLUGIN_GITHUB_TOKEN;
655
+ if (!token) {
461
656
  core.setFailed("Error: PLUGIN_GITHUB_TOKEN env is not set");
462
- return;
657
+ return null;
463
658
  }
464
- const body = github2.context.payload.inputs;
659
+ return token;
660
+ }
661
+ async function getInputsOrFail(pluginOptions) {
662
+ const githubContext = getGithubContext();
663
+ const body = githubContext.payload.inputs;
465
664
  const inputSchemaErrors = [...Value3.Errors(inputSchema, body)];
466
665
  if (inputSchemaErrors.length) {
467
666
  console.dir(inputSchemaErrors, { depth: null });
468
667
  core.setFailed(`Error: Invalid inputs payload: ${inputSchemaErrors.map((o) => o.message).join(", ")}`);
469
- return;
470
- }
471
- const signature = body.signature;
472
- if (!pluginOptions.bypassSignatureVerification && !await verifySignature(pluginOptions.kernelPublicKey, body, signature)) {
473
- core.setFailed(`Error: Invalid signature`);
474
- return;
668
+ return null;
475
669
  }
476
- const inputs = Value3.Decode(inputSchema, body);
477
- let config;
478
- if (pluginOptions.settingsSchema) {
479
- try {
480
- config = Value3.Decode(pluginOptions.settingsSchema, Value3.Default(pluginOptions.settingsSchema, inputs.settings));
481
- } catch (e) {
482
- console.dir(...Value3.Errors(pluginOptions.settingsSchema, inputs.settings), { depth: null });
483
- core.setFailed(`Error: Invalid settings provided.`);
484
- throw e;
670
+ if (!pluginOptions.bypassSignatureVerification) {
671
+ const signature = typeof body.signature === "string" ? body.signature : "";
672
+ if (!signature) {
673
+ core.setFailed("Error: Missing signature");
674
+ return null;
485
675
  }
486
- } else {
487
- config = inputs.settings;
488
- }
489
- let env;
490
- if (pluginOptions.envSchema) {
491
- try {
492
- env = Value3.Decode(pluginOptions.envSchema, Value3.Default(pluginOptions.envSchema, process.env));
493
- } catch (e) {
494
- console.dir(...Value3.Errors(pluginOptions.envSchema, process.env), { depth: null });
495
- core.setFailed(`Error: Invalid environment provided.`);
496
- throw e;
676
+ const isValid = await verifySignature(pluginOptions.kernelPublicKey, body, signature);
677
+ if (!isValid) {
678
+ core.setFailed("Error: Invalid signature");
679
+ return null;
497
680
  }
498
- } else {
499
- env = process.env;
500
681
  }
501
- const command = getCommand(inputs, pluginOptions);
502
- const context2 = {
682
+ return Value3.Decode(inputSchema, body);
683
+ }
684
+ function decodeWithSchema(schema, value, errorMessage) {
685
+ if (!schema) {
686
+ return { value };
687
+ }
688
+ try {
689
+ return { value: Value3.Decode(schema, Value3.Default(schema, value)) };
690
+ } catch (error) {
691
+ console.dir(...Value3.Errors(schema, value), { depth: null });
692
+ const err = new Error(errorMessage);
693
+ err.cause = error;
694
+ return { value: null, error: err };
695
+ }
696
+ }
697
+ async function createActionsPlugin(handler, options) {
698
+ const pluginOptions = getPluginOptions(options);
699
+ const pluginGithubToken = getDispatchTokenOrFail(pluginOptions);
700
+ if (pluginOptions.returnDataToKernel && !pluginGithubToken) {
701
+ return;
702
+ }
703
+ const inputs = await getInputsOrFail(pluginOptions);
704
+ if (!inputs) {
705
+ return;
706
+ }
707
+ const context = {
503
708
  eventName: inputs.eventName,
504
709
  payload: inputs.eventPayload,
505
- command,
710
+ command: null,
506
711
  authToken: inputs.authToken,
507
712
  ubiquityKernelToken: inputs.ubiquityKernelToken,
508
713
  octokit: new customOctokit({ auth: inputs.authToken }),
509
- config,
510
- env,
714
+ config: inputs.settings,
715
+ env: process.env,
511
716
  logger: new Logs(pluginOptions.logLevel),
512
717
  commentHandler: new CommentHandler()
513
718
  };
719
+ const configResult = decodeWithSchema(pluginOptions.settingsSchema, inputs.settings, "Error: Invalid settings provided.");
720
+ if (!configResult.value) {
721
+ await handleError(context, pluginOptions, configResult.error ?? new Error("Error: Invalid settings provided."));
722
+ return;
723
+ }
724
+ context.config = configResult.value;
725
+ const envResult = decodeWithSchema(pluginOptions.envSchema, process.env, "Error: Invalid environment provided.");
726
+ if (!envResult.value) {
727
+ await handleError(context, pluginOptions, envResult.error ?? new Error("Error: Invalid environment provided."));
728
+ return;
729
+ }
730
+ context.env = envResult.value;
514
731
  try {
515
- const result = await handler(context2);
732
+ context.command = getCommand(inputs, pluginOptions);
733
+ const result = await handler(context);
516
734
  core.setOutput("result", result);
517
- if (pluginOptions?.returnDataToKernel) {
735
+ if (pluginOptions.returnDataToKernel && pluginGithubToken) {
518
736
  await returnDataToKernel(pluginGithubToken, inputs.stateId, result);
519
737
  }
520
738
  } catch (error) {
521
- await handleError(context2, pluginOptions, error);
739
+ await handleError(context, pluginOptions, error);
522
740
  }
523
741
  }
524
742
  async function returnDataToKernel(repoToken, stateId, output) {
743
+ const githubContext = getGithubContext();
525
744
  const octokit = new customOctokit({ auth: repoToken });
526
745
  await octokit.rest.repos.createDispatchEvent({
527
- owner: github2.context.repo.owner,
528
- repo: github2.context.repo.repo,
746
+ owner: githubContext.repo.owner,
747
+ repo: githubContext.repo.repo,
529
748
  event_type: "return-data-to-ubiquity-os-kernel",
530
749
  client_payload: {
531
750
  state_id: stateId,
@@ -587,107 +806,17 @@ function processSegment(segment, extraTags, shouldCollapseEmptyLines) {
587
806
  return s;
588
807
  }
589
808
 
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
-
680
809
  // src/server.ts
681
810
  import { Value as Value4 } from "@sinclair/typebox/value";
682
811
  import { Logs as Logs2 } from "@ubiquity-os/ubiquity-os-logger";
683
812
  import { Hono } from "hono";
684
813
  import { env as honoEnv } from "hono/adapter";
685
814
  import { HTTPException } from "hono/http-exception";
686
- async function handleError2(context2, pluginOptions, error) {
815
+ async function handleError2(context, pluginOptions, error) {
687
816
  console.error(error);
688
- const loggerError = transformError(context2, error);
817
+ const loggerError = transformError(context, error);
689
818
  if (pluginOptions.postCommentOnError && loggerError) {
690
- await context2.commentHandler.postComment(context2, loggerError);
819
+ await context.commentHandler.postComment(context, loggerError);
691
820
  }
692
821
  throw new HTTPException(500, { message: "Unexpected error" });
693
822
  }
@@ -738,7 +867,7 @@ function createPlugin(handler, manifest, options) {
738
867
  const workerName = new URL(inputs.ref).hostname.split(".")[0];
739
868
  PluginRuntimeInfo.getInstance({ ...env, CLOUDFLARE_WORKER_NAME: workerName });
740
869
  const command = getCommand(inputs, pluginOptions);
741
- const context2 = {
870
+ const context = {
742
871
  eventName: inputs.eventName,
743
872
  payload: inputs.eventPayload,
744
873
  command,
@@ -751,14 +880,236 @@ function createPlugin(handler, manifest, options) {
751
880
  commentHandler: new CommentHandler()
752
881
  };
753
882
  try {
754
- const result = await handler(context2);
883
+ const result = await handler(context);
755
884
  return ctx.json({ stateId: inputs.stateId, output: result ?? {} });
756
885
  } catch (error) {
757
- await handleError2(context2, pluginOptions, error);
886
+ await handleError2(context, pluginOptions, error);
758
887
  }
759
888
  });
760
889
  return app;
761
890
  }
891
+
892
+ // src/llm/index.ts
893
+ var EMPTY_STRING = "";
894
+ function normalizeBaseUrl(baseUrl) {
895
+ let normalized = baseUrl.trim();
896
+ while (normalized.endsWith("/")) {
897
+ normalized = normalized.slice(0, -1);
898
+ }
899
+ return normalized;
900
+ }
901
+ var MAX_LLM_RETRIES = 2;
902
+ var RETRY_BACKOFF_MS = [250, 750];
903
+ function getRetryDelayMs(attempt) {
904
+ return RETRY_BACKOFF_MS[Math.min(attempt, RETRY_BACKOFF_MS.length - 1)] ?? 750;
905
+ }
906
+ function sleep(ms) {
907
+ return new Promise((resolve) => setTimeout(resolve, ms));
908
+ }
909
+ function getEnvString(name) {
910
+ if (typeof process === "undefined" || !process?.env) return EMPTY_STRING;
911
+ return String(process.env[name] ?? EMPTY_STRING).trim();
912
+ }
913
+ function normalizeToken(value) {
914
+ return typeof value === "string" ? value.trim() : EMPTY_STRING;
915
+ }
916
+ function isGitHubToken(token) {
917
+ return token.trim().startsWith("gh");
918
+ }
919
+ function getEnvTokenFromInput(input) {
920
+ if ("env" in input) {
921
+ const envValue = input.env;
922
+ if (envValue && typeof envValue === "object") {
923
+ const token = normalizeToken(envValue.UOS_AI_TOKEN);
924
+ if (token) return token;
925
+ }
926
+ }
927
+ return getEnvString("UOS_AI_TOKEN");
928
+ }
929
+ function resolveAuthToken(input, aiAuthToken) {
930
+ const explicit = normalizeToken(aiAuthToken);
931
+ if (explicit) return { token: explicit, isGitHub: isGitHubToken(explicit) };
932
+ const envToken = getEnvTokenFromInput(input);
933
+ if (envToken) return { token: envToken, isGitHub: isGitHubToken(envToken) };
934
+ const fallback = normalizeToken(input.authToken);
935
+ if (!fallback) {
936
+ const err = new Error("Missing auth token; set UOS_AI_TOKEN, pass aiAuthToken, or provide authToken in input");
937
+ err.status = 401;
938
+ throw err;
939
+ }
940
+ return { token: fallback, isGitHub: isGitHubToken(fallback) };
941
+ }
942
+ function getAiBaseUrl(options) {
943
+ if (typeof options.baseUrl === "string" && options.baseUrl.trim()) {
944
+ return normalizeBaseUrl(options.baseUrl);
945
+ }
946
+ const envBaseUrl = getEnvString("UOS_AI_URL");
947
+ if (envBaseUrl) return normalizeBaseUrl(envBaseUrl);
948
+ return "https://ai.ubq.fi";
949
+ }
950
+ async function callLlm(options, input) {
951
+ const { baseUrl, model, stream: isStream, messages, aiAuthToken, ...rest } = options;
952
+ const { token: authToken, isGitHub } = resolveAuthToken(input, aiAuthToken);
953
+ const kernelToken = "ubiquityKernelToken" in input ? input.ubiquityKernelToken : void 0;
954
+ const payload = getPayload(input);
955
+ const { owner, repo, installationId } = getRepoMetadata(payload);
956
+ ensureMessages(messages);
957
+ const url = buildAiUrl(options, baseUrl);
958
+ const body = JSON.stringify({
959
+ ...rest,
960
+ ...model ? { model } : {},
961
+ messages,
962
+ stream: isStream ?? false
963
+ });
964
+ const headers = buildHeaders(authToken, {
965
+ owner,
966
+ repo,
967
+ installationId,
968
+ ubiquityKernelToken: isGitHub ? kernelToken : void 0
969
+ });
970
+ const response = await fetchWithRetry(url, { method: "POST", headers, body }, MAX_LLM_RETRIES);
971
+ if (isStream) {
972
+ if (!response.body) {
973
+ throw new Error("LLM API error: missing response body for streaming request");
974
+ }
975
+ return parseSseStream(response.body);
976
+ }
977
+ const rawText = await response.text();
978
+ try {
979
+ return JSON.parse(rawText);
980
+ } catch (err) {
981
+ const preview = rawText ? rawText.slice(0, 1e3) : EMPTY_STRING;
982
+ const message = "LLM API error: failed to parse JSON response from server" + (preview ? `; response body (truncated): ${preview}` : EMPTY_STRING);
983
+ const error = new Error(message);
984
+ error.cause = err;
985
+ error.status = response.status;
986
+ throw error;
987
+ }
988
+ }
989
+ function ensureMessages(messages) {
990
+ if (!Array.isArray(messages) || messages.length === 0) {
991
+ const err = new Error("messages must be a non-empty array");
992
+ err.status = 400;
993
+ throw err;
994
+ }
995
+ }
996
+ function buildAiUrl(options, baseUrl) {
997
+ return `${getAiBaseUrl({ ...options, baseUrl })}/v1/chat/completions`;
998
+ }
999
+ async function fetchWithRetry(url, options, maxRetries) {
1000
+ let attempt = 0;
1001
+ let lastError;
1002
+ while (attempt <= maxRetries) {
1003
+ try {
1004
+ const response = await fetch(url, options);
1005
+ if (response.ok) return response;
1006
+ throw await buildResponseError(response);
1007
+ } catch (error) {
1008
+ lastError = error;
1009
+ if (!shouldRetryError(error, attempt, maxRetries)) throw error;
1010
+ await sleep(getRetryDelayMs(attempt));
1011
+ attempt += 1;
1012
+ }
1013
+ }
1014
+ throw lastError ?? new Error("LLM API error: request failed after retries");
1015
+ }
1016
+ async function buildResponseError(response) {
1017
+ const errText = await response.text();
1018
+ const error = new Error(`LLM API error: ${response.status} - ${errText}`);
1019
+ error.status = response.status;
1020
+ return error;
1021
+ }
1022
+ function shouldRetryError(error, attempt, maxRetries) {
1023
+ if (attempt >= maxRetries) return false;
1024
+ const status = getErrorStatus2(error);
1025
+ if (typeof status === "number") {
1026
+ return status >= 500;
1027
+ }
1028
+ return true;
1029
+ }
1030
+ function getErrorStatus2(error) {
1031
+ return typeof error?.status === "number" ? error.status : void 0;
1032
+ }
1033
+ function getPayload(input) {
1034
+ if ("payload" in input) {
1035
+ return input.payload;
1036
+ }
1037
+ return input.eventPayload;
1038
+ }
1039
+ function getRepoMetadata(payload) {
1040
+ const repoPayload = payload;
1041
+ return {
1042
+ owner: repoPayload?.repository?.owner?.login ?? EMPTY_STRING,
1043
+ repo: repoPayload?.repository?.name ?? EMPTY_STRING,
1044
+ installationId: repoPayload?.installation?.id
1045
+ };
1046
+ }
1047
+ function buildHeaders(authToken, options) {
1048
+ const headers = {
1049
+ Authorization: `Bearer ${authToken}`,
1050
+ "Content-Type": "application/json"
1051
+ };
1052
+ if (options.owner) headers["X-GitHub-Owner"] = options.owner;
1053
+ if (options.repo) headers["X-GitHub-Repo"] = options.repo;
1054
+ if (typeof options.installationId === "number" && Number.isFinite(options.installationId)) {
1055
+ headers["X-GitHub-Installation-Id"] = String(options.installationId);
1056
+ }
1057
+ if (options.ubiquityKernelToken) {
1058
+ headers["X-Ubiquity-Kernel-Token"] = options.ubiquityKernelToken;
1059
+ }
1060
+ return headers;
1061
+ }
1062
+ async function* parseSseStream(body) {
1063
+ const reader = body.getReader();
1064
+ const decoder = new TextDecoder();
1065
+ let buffer = EMPTY_STRING;
1066
+ try {
1067
+ while (true) {
1068
+ const { value, done: isDone } = await reader.read();
1069
+ if (isDone) break;
1070
+ buffer += decoder.decode(value, { stream: true });
1071
+ const { events, remainder } = splitSseEvents(buffer);
1072
+ buffer = remainder;
1073
+ for (const event of events) {
1074
+ const data = getEventData(event);
1075
+ if (!data) continue;
1076
+ if (data.trim() === "[DONE]") return;
1077
+ yield parseEventData(data);
1078
+ }
1079
+ }
1080
+ } finally {
1081
+ reader.releaseLock();
1082
+ }
1083
+ }
1084
+ function splitSseEvents(buffer) {
1085
+ const normalized = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1086
+ const parts = normalized.split("\n\n");
1087
+ const remainder = parts.pop() ?? EMPTY_STRING;
1088
+ return { events: parts, remainder };
1089
+ }
1090
+ function getEventData(event) {
1091
+ if (!event.trim()) return null;
1092
+ const dataLines = event.split("\n").filter((line) => line.startsWith("data:"));
1093
+ if (!dataLines.length) return null;
1094
+ const data = dataLines.map((line) => line.startsWith("data: ") ? line.slice(6) : line.slice(5).replace(/^ /, EMPTY_STRING)).join("\n");
1095
+ return data || null;
1096
+ }
1097
+ function parseEventData(data) {
1098
+ try {
1099
+ return JSON.parse(data);
1100
+ } catch (error) {
1101
+ if (data.includes("\n")) {
1102
+ const collapsed = data.replace(/\n/g, EMPTY_STRING);
1103
+ try {
1104
+ return JSON.parse(collapsed);
1105
+ } catch {
1106
+ }
1107
+ }
1108
+ const message = error instanceof Error ? error.message : String(error);
1109
+ const preview = data.length > 200 ? `${data.slice(0, 200)}...` : data;
1110
+ throw new Error(`LLM stream parse error: ${message}. Data: ${preview}`);
1111
+ }
1112
+ }
762
1113
  export {
763
1114
  CommentHandler,
764
1115
  callLlm,