@zhin.js/adapter-github 1.0.0 → 1.1.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.
Files changed (96) hide show
  1. package/CHANGELOG.md +1118 -0
  2. package/README.md +70 -191
  3. package/adapters/github.js +51 -0
  4. package/adapters/github.ts +59 -0
  5. package/agent/prompt-sections/platform.ts +16 -0
  6. package/{skills/github/SKILL.md → agent/skills/github.md} +26 -9
  7. package/agent/tools/bind.ts +13 -0
  8. package/agent/tools/create_pr.ts +20 -0
  9. package/agent/tools/install.ts +13 -0
  10. package/agent/tools/patch_file.ts +19 -0
  11. package/agent/tools/prepare_workspace.ts +15 -0
  12. package/agent/tools/push_branch.ts +18 -0
  13. package/agent/tools/star.ts +16 -0
  14. package/agent/tools/subscribe.ts +16 -0
  15. package/agent/tools/subscriptions.ts +13 -0
  16. package/agent/tools/unbind.ts +13 -0
  17. package/agent/tools/unsubscribe.ts +15 -0
  18. package/agent/tools/whoami.ts +13 -0
  19. package/commands/endpoint/add/[id].js +3 -0
  20. package/commands/endpoint/add/[id].ts +3 -0
  21. package/commands/endpoint/list.js +3 -0
  22. package/commands/endpoint/list.ts +3 -0
  23. package/commands/endpoint/remove/[id].js +3 -0
  24. package/commands/endpoint/remove/[id].ts +3 -0
  25. package/lib/client.d.ts +36 -0
  26. package/lib/client.js +47 -0
  27. package/lib/endpoint.d.ts +36 -0
  28. package/lib/endpoint.js +140 -0
  29. package/lib/gh-client.d.ts +73 -1
  30. package/lib/gh-client.js +109 -7
  31. package/lib/github-bot-handlers.d.ts +27 -0
  32. package/lib/github-bot-handlers.js +76 -0
  33. package/lib/github-channel-context.d.ts +16 -0
  34. package/lib/github-channel-context.js +31 -0
  35. package/lib/github-endpoint-commands.d.ts +1 -0
  36. package/lib/github-endpoint-commands.js +22 -0
  37. package/lib/github-runtime-state.d.ts +1 -0
  38. package/lib/github-runtime-state.js +6 -0
  39. package/lib/github-tool-handlers.d.ts +18 -0
  40. package/lib/github-tool-handlers.js +214 -0
  41. package/lib/index.d.ts +7 -32
  42. package/lib/index.js +7 -385
  43. package/lib/oauth-users.d.ts +33 -0
  44. package/lib/oauth-users.js +38 -0
  45. package/lib/protocol.d.ts +94 -0
  46. package/lib/protocol.js +292 -0
  47. package/lib/types.d.ts +9 -4
  48. package/lib/types.js +1 -2
  49. package/lib/webhook.d.ts +14 -0
  50. package/lib/webhook.js +88 -0
  51. package/lib/workspace-manager.d.ts +21 -0
  52. package/lib/workspace-manager.js +154 -0
  53. package/package.json +77 -22
  54. package/plugin.js +38 -0
  55. package/schema.json +130 -0
  56. package/src/client.ts +65 -0
  57. package/src/endpoint.ts +169 -0
  58. package/src/gh-client.ts +147 -12
  59. package/src/github-bot-handlers.ts +113 -0
  60. package/src/github-channel-context.ts +46 -0
  61. package/src/github-endpoint-commands.ts +23 -0
  62. package/src/github-runtime-state.ts +7 -0
  63. package/src/github-tool-handlers.ts +252 -0
  64. package/src/index.ts +48 -431
  65. package/src/oauth-users.ts +47 -0
  66. package/src/protocol.ts +425 -0
  67. package/src/types.ts +10 -4
  68. package/src/webhook.ts +130 -0
  69. package/src/workspace-manager.ts +168 -0
  70. package/lib/adapter.d.ts +0 -63
  71. package/lib/adapter.d.ts.map +0 -1
  72. package/lib/adapter.js +0 -415
  73. package/lib/adapter.js.map +0 -1
  74. package/lib/agent-prompt.d.ts +0 -3
  75. package/lib/agent-prompt.d.ts.map +0 -1
  76. package/lib/agent-prompt.js +0 -82
  77. package/lib/agent-prompt.js.map +0 -1
  78. package/lib/bot.d.ts +0 -26
  79. package/lib/bot.d.ts.map +0 -1
  80. package/lib/bot.js +0 -160
  81. package/lib/bot.js.map +0 -1
  82. package/lib/gh-client.d.ts.map +0 -1
  83. package/lib/gh-client.js.map +0 -1
  84. package/lib/index.d.ts.map +0 -1
  85. package/lib/index.js.map +0 -1
  86. package/lib/register-github-mcp.d.ts +0 -6
  87. package/lib/register-github-mcp.d.ts.map +0 -1
  88. package/lib/register-github-mcp.js +0 -35
  89. package/lib/register-github-mcp.js.map +0 -1
  90. package/lib/types.d.ts.map +0 -1
  91. package/lib/types.js.map +0 -1
  92. package/plugin.yml +0 -3
  93. package/src/adapter.ts +0 -446
  94. package/src/agent-prompt.ts +0 -99
  95. package/src/bot.ts +0 -174
  96. package/src/register-github-mcp.ts +0 -60
package/src/bot.ts DELETED
@@ -1,174 +0,0 @@
1
- /**
2
- * GitHub Bot 实现(基于 gh CLI)
3
- */
4
- import { formatCompact, Bot, Message, segment, SendContent, SendOptions, type MessageSegment } from 'zhin.js';
5
- import type {
6
- GitHubBotConfig,
7
- IssueCommentPayload,
8
- PRReviewCommentPayload,
9
- PRReviewPayload,
10
- } from './types.js';
11
- import { buildChannelId, parseChannelId } from './types.js';
12
- import type { GitHubAdapter } from './adapter.js';
13
- import { GhClient } from './gh-client.js';
14
-
15
- export function parseMarkdown(md: string): MessageSegment[] {
16
- const segments: MessageSegment[] = [];
17
- const mentionRe = /@(\w[-\w]*)/g;
18
- let lastIdx = 0;
19
- let match: RegExpExecArray | null;
20
- while ((match = mentionRe.exec(md)) !== null) {
21
- if (match.index > lastIdx) segments.push({ type: 'text', data: { text: md.slice(lastIdx, match.index) } });
22
- segments.push({ type: 'at', data: { id: match[1], name: match[1], text: match[0] } });
23
- lastIdx = match.index + match[0].length;
24
- }
25
- if (lastIdx < md.length) segments.push({ type: 'text', data: { text: md.slice(lastIdx) } });
26
- return segments.length ? segments : [{ type: 'text', data: { text: md } }];
27
- }
28
-
29
- export function toMarkdown(content: SendContent): string {
30
- if (!Array.isArray(content)) content = [content];
31
- return content.map(seg => {
32
- if (typeof seg === 'string') return seg;
33
- switch (seg.type) {
34
- case 'text': return seg.data.text || '';
35
- case 'at': return `@${seg.data.name || seg.data.id}`;
36
- case 'image': return seg.data.url ? `![image](${seg.data.url})` : '[image]';
37
- case 'link': return `[${seg.data.text || seg.data.url}](${seg.data.url})`;
38
- default: return seg.data?.text || `[${seg.type}]`;
39
- }
40
- }).join('');
41
- }
42
-
43
- export class GitHubBot implements Bot<GitHubBotConfig, IssueCommentPayload> {
44
- $connected = false;
45
- gh: GhClient;
46
-
47
- get $id() { return this.$config.name; }
48
-
49
- get logger() {
50
- return this.adapter.plugin.logger;
51
- }
52
-
53
- constructor(public adapter: GitHubAdapter, public $config: GitHubBotConfig) {
54
- const { host, app_id, private_key } = $config;
55
- const appAuth = app_id && private_key
56
- ? { appId: app_id, privateKey: private_key }
57
- : undefined;
58
- this.gh = new GhClient({ host, appAuth });
59
- }
60
-
61
- async $connect(): Promise<void> {
62
- const result = await this.gh.verifyAuth();
63
- if (!result.ok) throw new Error(`GitHub 认证失败: ${result.message}`);
64
- this.$connected = true;
65
- this.logger.info(formatCompact({ bot: this.$id }));
66
- }
67
-
68
- async $disconnect(): Promise<void> {
69
- this.$connected = false;
70
- this.logger.debug(formatCompact({ bot: this.$id, disconnect: true }));
71
- }
72
-
73
- $formatMessage(payload: IssueCommentPayload): Message<IssueCommentPayload> {
74
- const repo = payload.repository.full_name;
75
- const number = payload.issue.number;
76
- const isPR = 'pull_request' in (payload.issue as any);
77
- const channelId = buildChannelId(repo, isPR ? 'pr' : 'issue', number);
78
- const gh = this.gh;
79
-
80
- const result = Message.from(payload, {
81
- $id: payload.comment.id.toString(),
82
- $adapter: 'github',
83
- $bot: this.$config.name,
84
- $sender: { id: payload.sender.login, name: payload.sender.login },
85
- $channel: { id: channelId, type: 'group' },
86
- $content: parseMarkdown(payload.comment.body),
87
- $raw: payload.comment.body,
88
- $timestamp: new Date(payload.comment.created_at).getTime(),
89
- $recall: async () => { await gh.deleteIssueComment(repo, payload.comment.id); },
90
- $reply: async (content: SendContent, quote?: boolean | string): Promise<string> => {
91
- const text = toMarkdown(content);
92
- const finalBody = quote ? `> ${payload.comment.body.split('\n')[0]}\n\n${text}` : text;
93
- const r = await gh.createIssueComment(repo, number, finalBody);
94
- return r.ok ? r.data.id.toString() : '';
95
- },
96
- });
97
- return result;
98
- }
99
-
100
- formatPRReviewComment(payload: PRReviewCommentPayload): Message<PRReviewCommentPayload> {
101
- const repo = payload.repository.full_name;
102
- const number = payload.pull_request.number;
103
- const channelId = buildChannelId(repo, 'pr', number);
104
- const gh = this.gh;
105
-
106
- const body = payload.comment.path
107
- ? `**${payload.comment.path}**\n${payload.comment.diff_hunk ? '```diff\n' + payload.comment.diff_hunk + '\n```\n' : ''}${payload.comment.body}`
108
- : payload.comment.body;
109
-
110
- return Message.from(payload, {
111
- $id: payload.comment.id.toString(),
112
- $adapter: 'github',
113
- $bot: this.$config.name,
114
- $sender: { id: payload.sender.login, name: payload.sender.login },
115
- $channel: { id: channelId, type: 'group' },
116
- $content: parseMarkdown(body),
117
- $raw: body,
118
- $timestamp: new Date(payload.comment.created_at).getTime(),
119
- $recall: async () => { await gh.deletePRReviewComment(repo, payload.comment.id); },
120
- $reply: async (content: SendContent): Promise<string> => {
121
- const r = await gh.createPRComment(repo, number, toMarkdown(content));
122
- return r.ok ? r.data.id.toString() : '';
123
- },
124
- });
125
- }
126
-
127
- formatPRReview(payload: PRReviewPayload): Message<PRReviewPayload> | null {
128
- if (!payload.review.body) return null;
129
- const repo = payload.repository.full_name;
130
- const number = payload.pull_request.number;
131
- const channelId = buildChannelId(repo, 'pr', number);
132
- const gh = this.gh;
133
-
134
- const stateLabel: Record<string, string> = {
135
- approved: '✅ APPROVED', changes_requested: '🔄 CHANGES REQUESTED',
136
- commented: '💬 COMMENTED', dismissed: '❌ DISMISSED',
137
- };
138
- const body = `**[${stateLabel[payload.review.state] || payload.review.state}]**\n${payload.review.body}`;
139
-
140
- return Message.from(payload, {
141
- $id: payload.review.id.toString(),
142
- $adapter: 'github',
143
- $bot: this.$config.name,
144
- $sender: { id: payload.sender.login, name: payload.sender.login },
145
- $channel: { id: channelId, type: 'group' },
146
- $content: parseMarkdown(body),
147
- $raw: body,
148
- $timestamp: new Date(payload.review.submitted_at).getTime(),
149
- $recall: async () => {},
150
- $reply: async (content: SendContent): Promise<string> => {
151
- const r = await gh.createPRComment(repo, number, toMarkdown(content));
152
- return r.ok ? r.data.id.toString() : '';
153
- },
154
- });
155
- }
156
-
157
- async $sendMessage(options: SendOptions): Promise<string> {
158
- const parsed = parseChannelId(options.id);
159
- if (!parsed) throw new Error(`无效的 GitHub channel ID: ${options.id}`);
160
-
161
- const text = toMarkdown(options.content);
162
- const r = parsed.type === 'issue'
163
- ? await this.gh.createIssueComment(parsed.repo, parsed.number, text)
164
- : await this.gh.createPRComment(parsed.repo, parsed.number, text);
165
- if (!r.ok) throw new Error(`发送失败: ${JSON.stringify(r.data)}`);
166
-
167
- this.logger.debug(`${this.$id} send → ${options.id}: ${text.slice(0, 80)}...`);
168
- return r.data.id.toString();
169
- }
170
-
171
- async $recallMessage(id: string): Promise<void> {
172
- this.logger.warn(formatCompact( { op: 'recall', ok: false, error: 'use message.$recall()' }));
173
- }
174
- }
@@ -1,60 +0,0 @@
1
- /**
2
- * Register @modelcontextprotocol/server-github when a PAT is available.
3
- */
4
- import type { AIConfig, Plugin } from 'zhin.js';
5
-
6
- interface GithubMcpServerEntry {
7
- name: string;
8
- transport: 'stdio';
9
- command: string;
10
- args: string[];
11
- env: Record<string, string>;
12
- }
13
-
14
- interface AgentOrchestratorLike {
15
- mcps: { has(name: string): boolean };
16
- addMcp(config: GithubMcpServerEntry, scope?: object, source?: string): () => void;
17
- }
18
-
19
- function resolveGithubMcpToken(ai?: AIConfig): string | undefined {
20
- const fromConfig = ai?.githubMcp?.token?.trim();
21
- if (fromConfig) return fromConfig;
22
- return (
23
- process.env.GITHUB_PERSONAL_ACCESS_TOKEN?.trim() ||
24
- process.env.GITHUB_TOKEN?.trim() ||
25
- undefined
26
- );
27
- }
28
-
29
- export function registerGithubMcp(plugin: Plugin): void {
30
- const { useContext, root, logger } = plugin;
31
-
32
- useContext('ai', (ai) => {
33
- if (!ai?.isReady?.()) return;
34
-
35
- const orchestrator = root.inject('agent') as AgentOrchestratorLike | undefined;
36
- if (!orchestrator) return;
37
-
38
- if (orchestrator.mcps.has('github')) return;
39
-
40
- const configService = root.inject('config');
41
- const appConfig = configService?.getPrimary<{ ai?: AIConfig }>() || {};
42
- const token = resolveGithubMcpToken(appConfig.ai);
43
- if (!token) {
44
- logger.debug('[MCP] server-github skipped: set GITHUB_PERSONAL_ACCESS_TOKEN or ai.githubMcp.token');
45
- return;
46
- }
47
-
48
- return orchestrator.addMcp(
49
- {
50
- name: 'github',
51
- transport: 'stdio',
52
- command: 'npx',
53
- args: ['-y', '@modelcontextprotocol/server-github'],
54
- env: { GITHUB_PERSONAL_ACCESS_TOKEN: token },
55
- },
56
- {},
57
- 'adapter-github',
58
- );
59
- });
60
- }