koishi-plugin-githubsth 1.0.1-test8 → 1.0.2-beta1

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.
@@ -1,2 +1,2 @@
1
1
  import { Context } from 'koishi';
2
- export declare function apply(ctx: Context): void;
2
+ export declare function apply(ctx: Context, config: any): void;
@@ -1,11 +1,37 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.apply = apply;
4
- function apply(ctx) {
4
+ function apply(ctx, config) {
5
5
  const logger = ctx.logger('githubsth');
6
6
  const repoRegex = /^[\w-]+\/[\w-\.]+$/;
7
+ const validEvents = [
8
+ 'push', 'issues', 'issue_comment', 'pull_request',
9
+ 'pull_request_review', 'star', 'fork', 'release',
10
+ 'discussion', 'workflow_run'
11
+ ];
12
+ const defaultConfigEvents = config.defaultEvents || ['push', 'issues', 'issue_comment', 'pull_request', 'pull_request_review', 'release', 'star', 'fork'];
7
13
  ctx.command('githubsth.subscribe <repo> [events:text]', '订阅 GitHub 仓库')
8
14
  .alias('gh.sub')
15
+ .usage(`
16
+ 订阅 GitHub 仓库通知。
17
+ 如果不指定事件,默认订阅: ${defaultConfigEvents.join(', ')}
18
+
19
+ 可选事件:
20
+ - push: 代码推送
21
+ - issues: Issue 创建/关闭/重开
22
+ - issue_comment: Issue 评论
23
+ - pull_request: PR 创建/关闭/重开
24
+ - pull_request_review: PR 审查
25
+ - star: 标星
26
+ - fork: 仓库 Fork
27
+ - release: 发布新版本
28
+ - discussion: 讨论区更新
29
+ - workflow_run: Workflow 运行
30
+
31
+ 示例:
32
+ gh.sub koishijs/koishi
33
+ gh.sub koishijs/koishi push,issues,star
34
+ `)
9
35
  .action(async ({ session }, repo, eventsStr) => {
10
36
  if (!repo)
11
37
  return '请指定仓库名称 (owner/repo)。';
@@ -19,15 +45,46 @@ function apply(ctx) {
19
45
  return '该仓库不在信任列表中,无法订阅。请联系管理员添加。';
20
46
  }
21
47
  // Parse events
22
- const events = eventsStr ? eventsStr.split(',').map(e => e.trim()) : ['push', 'issues', 'pull_request']; // Default events
48
+ let events;
49
+ if (eventsStr) {
50
+ // Split by comma, Chinese comma, or whitespace
51
+ events = eventsStr.split(/[,,\s]+/).map(e => e.trim()).filter(Boolean);
52
+ // Normalize events (kebab-case to snake_case)
53
+ events = events.map(e => e.replace(/-/g, '_'));
54
+ // Validate events
55
+ const invalidEvents = events.filter(e => !validEvents.includes(e) && e !== '*');
56
+ if (invalidEvents.length > 0) {
57
+ return `无效的事件类型: ${invalidEvents.join(', ')}。\n可选事件: ${validEvents.join(', ')}`;
58
+ }
59
+ }
60
+ else {
61
+ // Default events
62
+ events = [...defaultConfigEvents];
63
+ }
23
64
  try {
24
- await ctx.database.create('github_subscription', {
65
+ // Check if subscription exists
66
+ const existing = await ctx.database.get('github_subscription', {
25
67
  repo,
26
68
  channelId: session.channelId,
27
69
  platform: session.platform || 'unknown',
28
- events,
29
70
  });
30
- return `已订阅 ${repo} ${events.join(', ')} 事件。`;
71
+ if (existing.length > 0) {
72
+ // Update existing subscription
73
+ await ctx.database.set('github_subscription', { id: existing[0].id }, {
74
+ events,
75
+ });
76
+ return `已更新 ${repo} 的订阅,当前监听事件: ${events.join(', ')}。`;
77
+ }
78
+ else {
79
+ // Create new subscription
80
+ await ctx.database.create('github_subscription', {
81
+ repo,
82
+ channelId: session.channelId,
83
+ platform: session.platform || 'unknown',
84
+ events,
85
+ });
86
+ return `已订阅 ${repo} 的 ${events.join(', ')} 事件。`;
87
+ }
31
88
  }
32
89
  catch (e) {
33
90
  logger.warn(e);
package/lib/config.d.ts CHANGED
@@ -9,6 +9,8 @@ export interface Config {
9
9
  defaultOwner?: string;
10
10
  defaultRepo?: string;
11
11
  debug: boolean;
12
+ logUnhandledEvents: boolean;
13
+ defaultEvents: string[];
12
14
  rules?: Rule[];
13
15
  }
14
16
  export declare const Config: Schema<Config>;
package/lib/config.js CHANGED
@@ -6,6 +6,10 @@ exports.Config = koishi_1.Schema.object({
6
6
  defaultOwner: koishi_1.Schema.string().description('默认仓库拥有者'),
7
7
  defaultRepo: koishi_1.Schema.string().description('默认仓库名称'),
8
8
  debug: koishi_1.Schema.boolean().default(false).description('启用调试模式,输出详细日志'),
9
+ logUnhandledEvents: koishi_1.Schema.boolean().default(false).description('是否记录未处理的 Webhook 事件 (Unknown events)'),
10
+ defaultEvents: koishi_1.Schema.array(koishi_1.Schema.string())
11
+ .default(['push', 'issues', 'issue_comment', 'pull_request', 'pull_request_review', 'release', 'star', 'fork'])
12
+ .description('默认订阅事件列表 (当不指定事件时使用)'),
9
13
  rules: koishi_1.Schema.array(koishi_1.Schema.object({
10
14
  repo: koishi_1.Schema.string().required(),
11
15
  channelId: koishi_1.Schema.string().required(),
package/lib/index.js CHANGED
@@ -41,7 +41,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.inject = exports.name = void 0;
43
43
  exports.apply = apply;
44
- const commands = __importStar(require("./commands"));
44
+ const commands_1 = require("./commands");
45
45
  const database = __importStar(require("./database"));
46
46
  const zh_CN_1 = __importDefault(require("./locales/zh-CN"));
47
47
  const notifier_1 = require("./services/notifier");
@@ -60,33 +60,12 @@ function apply(ctx, config) {
60
60
  // 数据库
61
61
  ctx.plugin(database);
62
62
  // 注册服务
63
- logger.info('Registering Formatter...');
64
63
  ctx.plugin(formatter_1.Formatter);
65
- logger.info('Registering Notifier...');
66
64
  ctx.plugin(notifier_1.Notifier, config);
67
- // Debug listener for raw events
68
- ctx.on('github/issues', (payload) => {
69
- logger.info('[DEBUG] Global listener caught github/issues');
70
- });
71
- ctx.on('github/opened', (payload) => {
72
- logger.info('[DEBUG] Global listener caught github/opened');
73
- });
74
- // Comprehensive debug listeners
75
- ctx.on('github/webhook', (payload) => {
76
- logger.info('[DEBUG] Global listener caught github/webhook');
77
- });
78
- // Middleware to log all sessions
79
- ctx.middleware((session, next) => {
80
- // Log any session event from github
81
- if (session.platform === 'github') {
82
- logger.info(`[DEBUG] Middleware saw session.type: ${session.type}, subtype: ${session.subtype}`);
83
- }
84
- return next();
85
- });
86
65
  // 注册命令
87
66
  // admin and subscribe are already loaded in commands/index.ts, remove duplicate loading here
88
67
  try {
89
- ctx.plugin(commands, config);
68
+ ctx.plugin(commands_1.apply, config);
90
69
  logger.info('Plugin loaded successfully');
91
70
  }
92
71
  catch (e) {
@@ -10,5 +10,6 @@ export declare class Notifier extends Service {
10
10
  constructor(ctx: Context, config: Config);
11
11
  private registerListeners;
12
12
  private handleEvent;
13
+ private patchPayloadForEvent;
13
14
  private sendMessage;
14
15
  }
@@ -23,12 +23,55 @@ class Notifier extends koishi_1.Service {
23
23
  this.ctx.on('github/workflow-run', (payload) => this.handleEvent('workflow_run', payload));
24
24
  this.ctx.on('github/issue_comment', (payload) => this.handleEvent('issue_comment', payload));
25
25
  this.ctx.on('github/issue-comment', (payload) => this.handleEvent('issue_comment', payload));
26
- this.ctx.on('github/pull_request_review', (payload) => this.handleEvent('pull_request_review', payload));
27
26
  this.ctx.on('github/pull-request-review', (payload) => this.handleEvent('pull_request_review', payload));
27
+ // Fallback: Listen to message-created for adapters that map webhooks to messages
28
+ this.ctx.on('message-created', (session) => {
29
+ if (session.platform !== 'github')
30
+ return;
31
+ // Try to find payload
32
+ const payload = session.payload || session.extra || session.data;
33
+ if (payload) {
34
+ if (this.config.debug) {
35
+ this.ctx.logger('githubsth').info('Found payload in session, attempting to handle');
36
+ }
37
+ // Infer event type
38
+ let eventType = 'unknown';
39
+ if (payload.issue && payload.comment)
40
+ eventType = 'issue_comment';
41
+ else if (payload.issue)
42
+ eventType = 'issues';
43
+ else if (payload.pull_request && payload.review)
44
+ eventType = 'pull_request_review';
45
+ else if (payload.pull_request)
46
+ eventType = 'pull_request';
47
+ else if (payload.commits)
48
+ eventType = 'push';
49
+ else if (payload.starred_at !== undefined || (payload.action === 'started'))
50
+ eventType = 'star'; // star event usually has action 'created' but check payload structure
51
+ else if (payload.forkee)
52
+ eventType = 'fork';
53
+ else if (payload.release)
54
+ eventType = 'release';
55
+ else if (payload.discussion)
56
+ eventType = 'discussion';
57
+ else if (payload.workflow_run)
58
+ eventType = 'workflow_run';
59
+ // Handle raw star event if it has repository info directly
60
+ else if (payload.repository && (payload.action === 'created' || payload.action === 'started'))
61
+ eventType = 'star';
62
+ if (eventType !== 'unknown') {
63
+ this.handleEvent(eventType, payload);
64
+ }
65
+ else if (this.config.logUnhandledEvents) {
66
+ this.ctx.logger('githubsth').info(`Unhandled payload structure. Keys: ${Object.keys(payload).join(', ')}`);
67
+ }
68
+ }
69
+ });
28
70
  }
29
71
  async handleEvent(event, payload) {
30
- // FORCE LOG for debugging
31
- this.ctx.logger('githubsth').info(`Received event: ${event}`);
72
+ if (this.config.debug) {
73
+ this.ctx.logger('githubsth').info(`Received event: ${event}`);
74
+ }
32
75
  // Check if payload is nested in an 'event' object (common in some adapter versions)
33
76
  // or if the event data is directly in payload
34
77
  const realPayload = payload.payload || payload;
@@ -43,24 +86,77 @@ class Notifier extends koishi_1.Service {
43
86
  if (!repoName && realPayload.pull_request?.base?.repo?.full_name) {
44
87
  repoName = realPayload.pull_request.base.repo.full_name;
45
88
  }
89
+ // Special handling for 'star' event (which might be 'watch' event with action 'started')
90
+ // The payload might be missing repository info in the main object but have it in the original session payload
91
+ if (!repoName && event === 'star') {
92
+ // Sometimes the repository info is at the root of the payload, not inside 'payload' property
93
+ if (payload.repository?.full_name) {
94
+ repoName = payload.repository.full_name;
95
+ }
96
+ }
46
97
  if (!repoName) {
47
- this.ctx.logger('githubsth').warn(`Missing repo info for event: ${event}`);
48
98
  if (this.config.debug) {
49
- this.ctx.logger('notifier').warn(`Event ${event} missing repository info. Keys: ${Object.keys(realPayload).join(', ')}`);
99
+ this.ctx.logger('githubsth').warn(`Missing repo info for event: ${event}. Keys: ${Object.keys(realPayload).join(', ')}`);
100
+ }
101
+ else if (this.config.logUnhandledEvents) {
102
+ // Log at warning level if repo info is missing and logUnhandledEvents is on
103
+ this.ctx.logger('githubsth').warn(`Missing repo info for event: ${event}. Keys: ${Object.keys(realPayload).join(', ')}`);
50
104
  }
51
105
  return;
52
106
  }
53
- this.ctx.logger('githubsth').info(`Processing event ${event} for ${repoName}`);
107
+ // Patch realPayload with extracted repo info if missing
108
+ // This is crucial for formatter to work correctly as it expects repository object
109
+ if (!realPayload.repository) {
110
+ realPayload.repository = { full_name: repoName };
111
+ }
112
+ else if (!realPayload.repository.full_name) {
113
+ realPayload.repository.full_name = repoName;
114
+ }
115
+ // Patch realPayload with sender info if missing (e.g. issues event)
116
+ if (!realPayload.sender) {
117
+ if (realPayload.issue?.user) {
118
+ realPayload.sender = realPayload.issue.user;
119
+ }
120
+ else if (realPayload.pull_request?.user) {
121
+ realPayload.sender = realPayload.pull_request.user;
122
+ }
123
+ else if (realPayload.discussion?.user) {
124
+ realPayload.sender = realPayload.discussion.user;
125
+ }
126
+ else if (realPayload.pusher) {
127
+ realPayload.sender = { login: realPayload.pusher.name || 'Pusher' };
128
+ }
129
+ else {
130
+ // Fallback sender
131
+ realPayload.sender = { login: 'GitHub' };
132
+ }
133
+ }
134
+ // Comprehensive patching for specific events to prevent formatter crashes
135
+ try {
136
+ this.patchPayloadForEvent(event, realPayload, repoName);
137
+ }
138
+ catch (e) {
139
+ this.ctx.logger('githubsth').warn(`Failed to patch payload for ${event}:`, e);
140
+ }
54
141
  if (this.config.debug) {
142
+ this.ctx.logger('githubsth').info(`Processing event ${event} for ${repoName}`);
55
143
  this.ctx.logger('notifier').info(`Received event ${event} for ${repoName}`);
56
144
  this.ctx.logger('notifier').debug(JSON.stringify(realPayload, null, 2));
57
145
  }
58
146
  // Get rules from database
147
+ // Try to match both exact name and lowercase name to handle case sensitivity
148
+ const repoNames = [repoName];
149
+ if (repoName !== repoName.toLowerCase()) {
150
+ repoNames.push(repoName.toLowerCase());
151
+ }
59
152
  const dbRules = await this.ctx.database.get('github_subscription', {
60
- repo: repoName
153
+ repo: repoNames
61
154
  });
62
155
  // Combine with config rules (if any, for backward compatibility or static rules)
63
- const configRules = (this.config.rules || []).filter((r) => r.repo === repoName || r.repo === '*');
156
+ // Also match config rules case-insensitively if needed
157
+ const configRules = (this.config.rules || []).filter((r) => r.repo === repoName ||
158
+ r.repo === repoName.toLowerCase() ||
159
+ r.repo === '*');
64
160
  const allRules = [
65
161
  ...dbRules.map(r => ({ ...r, platform: r.platform })),
66
162
  ...configRules
@@ -72,14 +168,17 @@ class Notifier extends koishi_1.Service {
72
168
  return true;
73
169
  });
74
170
  if (matchedRules.length === 0) {
75
- this.ctx.logger('githubsth').info(`No matching rules for ${repoName} (event: ${event})`);
76
171
  if (this.config.debug) {
172
+ this.ctx.logger('githubsth').info(`No matching rules for ${repoName} (event: ${event})`);
77
173
  this.ctx.logger('notifier').debug(`No matching rules for ${repoName} (event: ${event})`);
78
174
  }
175
+ else if (this.config.logUnhandledEvents) {
176
+ this.ctx.logger('githubsth').warn(`No matching rules for ${repoName} (event: ${event})`);
177
+ }
79
178
  return;
80
179
  }
81
- this.ctx.logger('githubsth').info(`Found ${matchedRules.length} matching rules for ${repoName}`);
82
180
  if (this.config.debug) {
181
+ this.ctx.logger('githubsth').info(`Found ${matchedRules.length} matching rules for ${repoName}`);
83
182
  this.ctx.logger('notifier').debug(`Found ${matchedRules.length} matching rules for ${repoName}`);
84
183
  }
85
184
  let message = null;
@@ -88,50 +187,160 @@ class Notifier extends koishi_1.Service {
88
187
  this.ctx.logger('notifier').warn('Formatter service not available');
89
188
  return;
90
189
  }
190
+ try {
191
+ switch (event) {
192
+ case 'push':
193
+ message = this.ctx.githubsthFormatter.formatPush(realPayload);
194
+ break;
195
+ case 'issues':
196
+ message = this.ctx.githubsthFormatter.formatIssue(realPayload);
197
+ break;
198
+ case 'pull_request':
199
+ message = this.ctx.githubsthFormatter.formatPullRequest(realPayload);
200
+ break;
201
+ case 'star':
202
+ message = this.ctx.githubsthFormatter.formatStar(realPayload);
203
+ break;
204
+ case 'fork':
205
+ message = this.ctx.githubsthFormatter.formatFork(realPayload);
206
+ break;
207
+ case 'release':
208
+ message = this.ctx.githubsthFormatter.formatRelease(realPayload);
209
+ break;
210
+ case 'discussion':
211
+ message = this.ctx.githubsthFormatter.formatDiscussion(realPayload);
212
+ break;
213
+ case 'workflow_run':
214
+ message = this.ctx.githubsthFormatter.formatWorkflowRun(realPayload);
215
+ break;
216
+ case 'issue_comment':
217
+ message = this.ctx.githubsthFormatter.formatIssueComment(realPayload);
218
+ break;
219
+ case 'pull_request_review':
220
+ message = this.ctx.githubsthFormatter.formatPullRequestReview(realPayload);
221
+ break;
222
+ }
223
+ }
224
+ catch (e) {
225
+ this.ctx.logger('githubsth').error(`Error formatting event ${event}:`, e);
226
+ if (this.config.debug) {
227
+ this.ctx.logger('notifier').error(`Error formatting event ${event}:`, e);
228
+ }
229
+ return;
230
+ }
231
+ if (!message) {
232
+ if (this.config.debug) {
233
+ this.ctx.logger('notifier').debug(`Formatter returned null for event ${event}`);
234
+ }
235
+ return;
236
+ }
237
+ for (const rule of matchedRules) {
238
+ if (this.config.debug) {
239
+ this.ctx.logger('notifier').debug(`Sending message to channel ${rule.channelId} (platform: ${rule.platform || 'any'})`);
240
+ }
241
+ await this.sendMessage(rule, message);
242
+ }
243
+ }
244
+ patchPayloadForEvent(event, payload, repoName) {
245
+ // Ensure sender exists (handled before, but good for type safety)
246
+ const defaultUser = { login: 'GitHub', id: 0, avatar_url: '' };
247
+ if (!payload.sender)
248
+ payload.sender = defaultUser;
249
+ // Ensure repository exists (handled before, but good for type safety)
250
+ const defaultRepo = { full_name: repoName, stargazers_count: 0, html_url: `https://github.com/${repoName}` };
251
+ if (!payload.repository)
252
+ payload.repository = defaultRepo;
91
253
  switch (event) {
92
254
  case 'push':
93
- message = this.ctx.githubsthFormatter.formatPush(realPayload);
255
+ if (!payload.pusher)
256
+ payload.pusher = { name: payload.sender.login };
257
+ if (!payload.commits)
258
+ payload.commits = [];
259
+ if (!payload.ref)
260
+ payload.ref = 'refs/heads/unknown';
261
+ if (!payload.compare)
262
+ payload.compare = '';
263
+ // Ensure author exists in commits
264
+ if (payload.commits.length > 0) {
265
+ payload.commits.forEach((c) => {
266
+ if (!c.author)
267
+ c.author = { name: 'Unknown' };
268
+ if (!c.id)
269
+ c.id = '0000000';
270
+ if (!c.message)
271
+ c.message = 'No message';
272
+ });
273
+ }
94
274
  break;
95
275
  case 'issues':
96
- message = this.ctx.githubsthFormatter.formatIssue(realPayload);
276
+ if (!payload.action)
277
+ payload.action = 'updated';
278
+ if (!payload.issue)
279
+ payload.issue = { number: 0, title: 'Unknown Issue', html_url: '', user: payload.sender };
280
+ // Ensure user exists in issue
281
+ if (!payload.issue.user)
282
+ payload.issue.user = payload.sender;
97
283
  break;
98
284
  case 'pull_request':
99
- message = this.ctx.githubsthFormatter.formatPullRequest(realPayload);
285
+ if (!payload.action)
286
+ payload.action = 'updated';
287
+ if (!payload.pull_request)
288
+ payload.pull_request = { number: 0, title: 'Unknown PR', state: 'unknown', html_url: '', user: payload.sender };
289
+ if (!payload.pull_request.user)
290
+ payload.pull_request.user = payload.sender;
100
291
  break;
101
292
  case 'star':
102
- message = this.ctx.githubsthFormatter.formatStar(realPayload);
293
+ if (!payload.action)
294
+ payload.action = 'created';
295
+ if (payload.repository && payload.repository.stargazers_count === undefined) {
296
+ payload.repository.stargazers_count = '?';
297
+ }
103
298
  break;
104
299
  case 'fork':
105
- message = this.ctx.githubsthFormatter.formatFork(realPayload);
300
+ if (!payload.forkee)
301
+ payload.forkee = { full_name: 'unknown/fork' };
106
302
  break;
107
303
  case 'release':
108
- message = this.ctx.githubsthFormatter.formatRelease(realPayload);
304
+ if (!payload.action)
305
+ payload.action = 'published';
306
+ if (!payload.release)
307
+ payload.release = { tag_name: 'unknown', name: 'Unknown Release', html_url: '' };
109
308
  break;
110
309
  case 'discussion':
111
- message = this.ctx.githubsthFormatter.formatDiscussion(realPayload);
310
+ if (!payload.action)
311
+ payload.action = 'updated';
312
+ if (!payload.discussion)
313
+ payload.discussion = { number: 0, title: 'Unknown Discussion', html_url: '', user: payload.sender };
314
+ if (!payload.discussion.user)
315
+ payload.discussion.user = payload.sender;
112
316
  break;
113
317
  case 'workflow_run':
114
- message = this.ctx.githubsthFormatter.formatWorkflowRun(realPayload);
318
+ if (!payload.action)
319
+ payload.action = 'completed';
320
+ if (!payload.workflow_run)
321
+ payload.workflow_run = { conclusion: 'unknown', name: 'Unknown Workflow', head_branch: 'unknown', html_url: '' };
115
322
  break;
116
323
  case 'issue_comment':
117
- message = this.ctx.githubsthFormatter.formatIssueComment(realPayload);
324
+ if (!payload.action)
325
+ payload.action = 'created';
326
+ if (!payload.issue)
327
+ payload.issue = { number: 0, title: 'Unknown Issue', html_url: '', user: payload.sender };
328
+ if (!payload.comment)
329
+ payload.comment = { body: '', html_url: '' };
330
+ if (!payload.issue.user)
331
+ payload.issue.user = payload.sender;
118
332
  break;
119
333
  case 'pull_request_review':
120
- message = this.ctx.githubsthFormatter.formatPullRequestReview(realPayload);
334
+ if (!payload.action)
335
+ payload.action = 'submitted';
336
+ if (!payload.pull_request)
337
+ payload.pull_request = { number: 0, title: 'Unknown PR', html_url: '', user: payload.sender };
338
+ if (!payload.review)
339
+ payload.review = { state: 'unknown', html_url: '' };
340
+ if (!payload.pull_request.user)
341
+ payload.pull_request.user = payload.sender;
121
342
  break;
122
343
  }
123
- if (!message) {
124
- if (this.config.debug) {
125
- this.ctx.logger('notifier').debug(`Formatter returned null for event ${event}`);
126
- }
127
- return;
128
- }
129
- for (const rule of matchedRules) {
130
- if (this.config.debug) {
131
- this.ctx.logger('notifier').debug(`Sending message to channel ${rule.channelId} (platform: ${rule.platform || 'any'})`);
132
- }
133
- await this.sendMessage(rule, message);
134
- }
135
344
  }
136
345
  async sendMessage(rule, message) {
137
346
  // Find suitable bots
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-githubsth",
3
- "version": "1.0.1-test8",
3
+ "version": "1.0.2-beta1",
4
4
  "description": "Github Subscriptions Notifications, push notifications for GitHub subscriptions For koishi",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",