@geminixiang/mikan 1.0.0-beta.6 → 1.0.0-beta.8
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/CHANGELOG.md +31 -0
- package/dist/adapters/github/bot.d.ts +41 -0
- package/dist/adapters/github/bot.d.ts.map +1 -0
- package/dist/adapters/github/bot.js +429 -0
- package/dist/adapters/github/bot.js.map +1 -0
- package/dist/adapters/github/client.d.ts +53 -0
- package/dist/adapters/github/client.d.ts.map +1 -0
- package/dist/adapters/github/client.js +149 -0
- package/dist/adapters/github/client.js.map +1 -0
- package/dist/adapters/github/context.d.ts +9 -0
- package/dist/adapters/github/context.d.ts.map +1 -0
- package/dist/adapters/github/context.js +160 -0
- package/dist/adapters/github/context.js.map +1 -0
- package/dist/adapters/github/ids.d.ts +25 -0
- package/dist/adapters/github/ids.d.ts.map +1 -0
- package/dist/adapters/github/ids.js +39 -0
- package/dist/adapters/github/ids.js.map +1 -0
- package/dist/adapters/github/types.d.ts +79 -0
- package/dist/adapters/github/types.d.ts.map +1 -0
- package/dist/adapters/github/types.js +2 -0
- package/dist/adapters/github/types.js.map +1 -0
- package/dist/cli/ext-git.d.ts +22 -0
- package/dist/cli/ext-git.d.ts.map +1 -0
- package/dist/cli/ext-git.js +76 -0
- package/dist/cli/ext-git.js.map +1 -0
- package/dist/cli/ext.d.ts +2 -0
- package/dist/cli/ext.d.ts.map +1 -0
- package/dist/cli/ext.js +217 -0
- package/dist/cli/ext.js.map +1 -0
- package/dist/harness/extensions/loader.d.ts +28 -4
- package/dist/harness/extensions/loader.d.ts.map +1 -1
- package/dist/harness/extensions/loader.js +195 -44
- package/dist/harness/extensions/loader.js.map +1 -1
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.d.ts.map +1 -1
- package/dist/harness/index.js +1 -1
- package/dist/harness/index.js.map +1 -1
- package/dist/main.js +50 -2
- package/dist/main.js.map +1 -1
- package/dist/sessions/policy.d.ts.map +1 -1
- package/dist/sessions/policy.js +1 -0
- package/dist/sessions/policy.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createSign } from "crypto";
|
|
2
|
+
export class GithubApiError extends Error {
|
|
3
|
+
constructor(status, method, path, detail) {
|
|
4
|
+
super(`GitHub ${method} ${path} failed with ${status}: ${detail}`);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.name = "GithubApiError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* GitHub rate-limit responses are 429, or 403 with a rate-limit message
|
|
11
|
+
* (secondary limits). Used as the `withRetry` predicate.
|
|
12
|
+
*/
|
|
13
|
+
export function githubIsRateLimited(err) {
|
|
14
|
+
if (!(err instanceof GithubApiError))
|
|
15
|
+
return false;
|
|
16
|
+
return err.status === 429 || (err.status === 403 && /rate limit/i.test(err.message));
|
|
17
|
+
}
|
|
18
|
+
function base64Url(data) {
|
|
19
|
+
return (typeof data === "string" ? Buffer.from(data) : data).toString("base64url");
|
|
20
|
+
}
|
|
21
|
+
/** Refresh the cached installation token this long before it expires. */
|
|
22
|
+
const TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000;
|
|
23
|
+
/**
|
|
24
|
+
* Minimal GitHub REST client authenticated as a GitHub App installation.
|
|
25
|
+
* Mints short-lived installation tokens from an RS256 app JWT and supports
|
|
26
|
+
* ETag conditional polling (304 responses don't count against the rate limit).
|
|
27
|
+
*/
|
|
28
|
+
export class GithubClient {
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.installationToken = null;
|
|
31
|
+
this.etags = new Map();
|
|
32
|
+
this.appId = options.appId;
|
|
33
|
+
this.privateKey = options.privateKey;
|
|
34
|
+
this.installationId = options.installationId;
|
|
35
|
+
this.baseUrl = (options.baseUrl ?? "https://api.github.com").replace(/\/+$/, "");
|
|
36
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
37
|
+
}
|
|
38
|
+
/** Short-lived JWT identifying the App itself (not an installation). */
|
|
39
|
+
appJwt() {
|
|
40
|
+
const now = Math.floor(Date.now() / 1000);
|
|
41
|
+
const header = base64Url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
42
|
+
// 60s clock-drift backdate; GitHub caps exp at 10 minutes.
|
|
43
|
+
const payload = base64Url(JSON.stringify({ iat: now - 60, exp: now + 540, iss: this.appId }));
|
|
44
|
+
const signature = base64Url(createSign("RSA-SHA256").update(`${header}.${payload}`).sign(this.privateKey));
|
|
45
|
+
return `${header}.${payload}.${signature}`;
|
|
46
|
+
}
|
|
47
|
+
async getInstallationToken() {
|
|
48
|
+
const cached = this.installationToken;
|
|
49
|
+
if (cached && Date.now() < cached.expiresAt - TOKEN_REFRESH_MARGIN_MS) {
|
|
50
|
+
return cached.value;
|
|
51
|
+
}
|
|
52
|
+
const data = await this.rawRequest("POST", `/app/installations/${this.installationId}/access_tokens`, { auth: `Bearer ${this.appJwt()}` });
|
|
53
|
+
this.installationToken = { value: data.token, expiresAt: Date.parse(data.expires_at) };
|
|
54
|
+
return this.installationToken.value;
|
|
55
|
+
}
|
|
56
|
+
async rawRequest(method, path, options) {
|
|
57
|
+
const headers = {
|
|
58
|
+
Accept: "application/vnd.github+json",
|
|
59
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
60
|
+
"User-Agent": "mikan",
|
|
61
|
+
Authorization: options.auth,
|
|
62
|
+
};
|
|
63
|
+
if (options.conditional) {
|
|
64
|
+
const etag = this.etags.get(path);
|
|
65
|
+
if (etag)
|
|
66
|
+
headers["If-None-Match"] = etag;
|
|
67
|
+
}
|
|
68
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
69
|
+
method,
|
|
70
|
+
headers,
|
|
71
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
72
|
+
});
|
|
73
|
+
if (response.status === 304) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const detail = (await response.text().catch(() => "")).slice(0, 300);
|
|
78
|
+
throw new GithubApiError(response.status, method, path, detail || response.statusText);
|
|
79
|
+
}
|
|
80
|
+
if (options.conditional) {
|
|
81
|
+
const etag = response.headers.get("etag");
|
|
82
|
+
if (etag)
|
|
83
|
+
this.etags.set(path, etag);
|
|
84
|
+
}
|
|
85
|
+
if (response.status === 204) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return (await response.json());
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Installation-authenticated request. `conditional` GETs return null on 304
|
|
92
|
+
* (nothing changed since the last call to the same path).
|
|
93
|
+
*/
|
|
94
|
+
async request(method, path, options = {}) {
|
|
95
|
+
const token = await this.getInstallationToken();
|
|
96
|
+
return this.rawRequest(method, path, { ...options, auth: `Bearer ${token}` });
|
|
97
|
+
}
|
|
98
|
+
/** The App's mention slug; its comments author as `<slug>[bot]`. */
|
|
99
|
+
async getAppSlug() {
|
|
100
|
+
const app = await this.rawRequest("GET", "/app", {
|
|
101
|
+
auth: `Bearer ${this.appJwt()}`,
|
|
102
|
+
});
|
|
103
|
+
return app.slug;
|
|
104
|
+
}
|
|
105
|
+
async listInstallationRepositories() {
|
|
106
|
+
const data = await this.request("GET", "/installation/repositories?per_page=100");
|
|
107
|
+
return data.repositories;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* All issue/PR conversation comments in a repo updated at or after `since`,
|
|
111
|
+
* oldest first. Returns null when nothing changed (ETag 304). Capped at one
|
|
112
|
+
* page; the poll cursor advances to the newest item seen, so a burst larger
|
|
113
|
+
* than 100 comments drains across consecutive polls.
|
|
114
|
+
*/
|
|
115
|
+
async listIssueCommentsSince(owner, repo, since) {
|
|
116
|
+
return this.request("GET", `/repos/${owner}/${repo}/issues/comments?sort=updated&direction=asc&since=${encodeURIComponent(since)}&per_page=100`, { conditional: true });
|
|
117
|
+
}
|
|
118
|
+
/** Issues and PRs updated at or after `since` (GitHub lists PRs as issues). */
|
|
119
|
+
async listIssuesSince(owner, repo, since) {
|
|
120
|
+
return this.request("GET", `/repos/${owner}/${repo}/issues?state=all&sort=updated&direction=asc&since=${encodeURIComponent(since)}&per_page=100`, { conditional: true });
|
|
121
|
+
}
|
|
122
|
+
async getIssue(owner, repo, number) {
|
|
123
|
+
const issue = await this.request("GET", `/repos/${owner}/${repo}/issues/${number}`);
|
|
124
|
+
return issue;
|
|
125
|
+
}
|
|
126
|
+
async createIssueComment(owner, repo, number, body) {
|
|
127
|
+
const comment = await this.request("POST", `/repos/${owner}/${repo}/issues/${number}/comments`, { body: { body } });
|
|
128
|
+
return comment;
|
|
129
|
+
}
|
|
130
|
+
async updateIssueComment(owner, repo, commentId, body) {
|
|
131
|
+
await this.request("PATCH", `/repos/${owner}/${repo}/issues/comments/${commentId}`, {
|
|
132
|
+
body: { body },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async deleteIssueComment(owner, repo, commentId) {
|
|
136
|
+
await this.request("DELETE", `/repos/${owner}/${repo}/issues/comments/${commentId}`);
|
|
137
|
+
}
|
|
138
|
+
async createCommentReaction(owner, repo, commentId, content) {
|
|
139
|
+
await this.request("POST", `/repos/${owner}/${repo}/issues/comments/${commentId}/reactions`, {
|
|
140
|
+
body: { content },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
async createIssueReaction(owner, repo, number, content) {
|
|
144
|
+
await this.request("POST", `/repos/${owner}/${repo}/issues/${number}/reactions`, {
|
|
145
|
+
body: { content },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/adapters/github/client.ts"],"names":[],"mappings":"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","sourcesContent":["import { createSign } from \"crypto\";\nimport type {\n GithubClientOptions,\n GithubIssue,\n GithubIssueComment,\n GithubReactionContent,\n GithubRepository,\n} from \"./types.js\";\n\nexport class GithubApiError extends Error {\n constructor(\n public readonly status: number,\n method: string,\n path: string,\n detail: string,\n ) {\n super(`GitHub ${method} ${path} failed with ${status}: ${detail}`);\n this.name = \"GithubApiError\";\n }\n}\n\n/**\n * GitHub rate-limit responses are 429, or 403 with a rate-limit message\n * (secondary limits). Used as the `withRetry` predicate.\n */\nexport function githubIsRateLimited(err: Error): boolean {\n if (!(err instanceof GithubApiError)) return false;\n return err.status === 429 || (err.status === 403 && /rate limit/i.test(err.message));\n}\n\nfunction base64Url(data: string | Buffer): string {\n return (typeof data === \"string\" ? Buffer.from(data) : data).toString(\"base64url\");\n}\n\n/** Refresh the cached installation token this long before it expires. */\nconst TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000;\n\n/**\n * Minimal GitHub REST client authenticated as a GitHub App installation.\n * Mints short-lived installation tokens from an RS256 app JWT and supports\n * ETag conditional polling (304 responses don't count against the rate limit).\n */\nexport class GithubClient {\n private readonly appId: string;\n private readonly privateKey: string;\n private readonly installationId: string;\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n private installationToken: { value: string; expiresAt: number } | null = null;\n private etags = new Map<string, string>();\n\n constructor(options: GithubClientOptions) {\n this.appId = options.appId;\n this.privateKey = options.privateKey;\n this.installationId = options.installationId;\n this.baseUrl = (options.baseUrl ?? \"https://api.github.com\").replace(/\\/+$/, \"\");\n this.fetchImpl = options.fetchImpl ?? fetch;\n }\n\n /** Short-lived JWT identifying the App itself (not an installation). */\n private appJwt(): string {\n const now = Math.floor(Date.now() / 1000);\n const header = base64Url(JSON.stringify({ alg: \"RS256\", typ: \"JWT\" }));\n // 60s clock-drift backdate; GitHub caps exp at 10 minutes.\n const payload = base64Url(JSON.stringify({ iat: now - 60, exp: now + 540, iss: this.appId }));\n const signature = base64Url(\n createSign(\"RSA-SHA256\").update(`${header}.${payload}`).sign(this.privateKey),\n );\n return `${header}.${payload}.${signature}`;\n }\n\n private async getInstallationToken(): Promise<string> {\n const cached = this.installationToken;\n if (cached && Date.now() < cached.expiresAt - TOKEN_REFRESH_MARGIN_MS) {\n return cached.value;\n }\n const data = await this.rawRequest<{ token: string; expires_at: string }>(\n \"POST\",\n `/app/installations/${this.installationId}/access_tokens`,\n { auth: `Bearer ${this.appJwt()}` },\n );\n this.installationToken = { value: data!.token, expiresAt: Date.parse(data!.expires_at) };\n return this.installationToken.value;\n }\n\n private async rawRequest<T>(\n method: string,\n path: string,\n options: { auth: string; body?: unknown; conditional?: boolean },\n ): Promise<T | null> {\n const headers: Record<string, string> = {\n Accept: \"application/vnd.github+json\",\n \"X-GitHub-Api-Version\": \"2022-11-28\",\n \"User-Agent\": \"mikan\",\n Authorization: options.auth,\n };\n if (options.conditional) {\n const etag = this.etags.get(path);\n if (etag) headers[\"If-None-Match\"] = etag;\n }\n const response = await this.fetchImpl(`${this.baseUrl}${path}`, {\n method,\n headers,\n body: options.body !== undefined ? JSON.stringify(options.body) : undefined,\n });\n if (response.status === 304) {\n return null;\n }\n if (!response.ok) {\n const detail = (await response.text().catch(() => \"\")).slice(0, 300);\n throw new GithubApiError(response.status, method, path, detail || response.statusText);\n }\n if (options.conditional) {\n const etag = response.headers.get(\"etag\");\n if (etag) this.etags.set(path, etag);\n }\n if (response.status === 204) {\n return null;\n }\n return (await response.json()) as T;\n }\n\n /**\n * Installation-authenticated request. `conditional` GETs return null on 304\n * (nothing changed since the last call to the same path).\n */\n private async request<T>(\n method: string,\n path: string,\n options: { body?: unknown; conditional?: boolean } = {},\n ): Promise<T | null> {\n const token = await this.getInstallationToken();\n return this.rawRequest<T>(method, path, { ...options, auth: `Bearer ${token}` });\n }\n\n /** The App's mention slug; its comments author as `<slug>[bot]`. */\n async getAppSlug(): Promise<string> {\n const app = await this.rawRequest<{ slug: string }>(\"GET\", \"/app\", {\n auth: `Bearer ${this.appJwt()}`,\n });\n return app!.slug;\n }\n\n async listInstallationRepositories(): Promise<GithubRepository[]> {\n const data = await this.request<{ repositories: GithubRepository[] }>(\n \"GET\",\n \"/installation/repositories?per_page=100\",\n );\n return data!.repositories;\n }\n\n /**\n * All issue/PR conversation comments in a repo updated at or after `since`,\n * oldest first. Returns null when nothing changed (ETag 304). Capped at one\n * page; the poll cursor advances to the newest item seen, so a burst larger\n * than 100 comments drains across consecutive polls.\n */\n async listIssueCommentsSince(\n owner: string,\n repo: string,\n since: string,\n ): Promise<GithubIssueComment[] | null> {\n return this.request<GithubIssueComment[]>(\n \"GET\",\n `/repos/${owner}/${repo}/issues/comments?sort=updated&direction=asc&since=${encodeURIComponent(since)}&per_page=100`,\n { conditional: true },\n );\n }\n\n /** Issues and PRs updated at or after `since` (GitHub lists PRs as issues). */\n async listIssuesSince(owner: string, repo: string, since: string): Promise<GithubIssue[] | null> {\n return this.request<GithubIssue[]>(\n \"GET\",\n `/repos/${owner}/${repo}/issues?state=all&sort=updated&direction=asc&since=${encodeURIComponent(since)}&per_page=100`,\n { conditional: true },\n );\n }\n\n async getIssue(owner: string, repo: string, number: number): Promise<GithubIssue> {\n const issue = await this.request<GithubIssue>(\n \"GET\",\n `/repos/${owner}/${repo}/issues/${number}`,\n );\n return issue!;\n }\n\n async createIssueComment(\n owner: string,\n repo: string,\n number: number,\n body: string,\n ): Promise<GithubIssueComment> {\n const comment = await this.request<GithubIssueComment>(\n \"POST\",\n `/repos/${owner}/${repo}/issues/${number}/comments`,\n { body: { body } },\n );\n return comment!;\n }\n\n async updateIssueComment(\n owner: string,\n repo: string,\n commentId: number,\n body: string,\n ): Promise<void> {\n await this.request(\"PATCH\", `/repos/${owner}/${repo}/issues/comments/${commentId}`, {\n body: { body },\n });\n }\n\n async deleteIssueComment(owner: string, repo: string, commentId: number): Promise<void> {\n await this.request(\"DELETE\", `/repos/${owner}/${repo}/issues/comments/${commentId}`);\n }\n\n async createCommentReaction(\n owner: string,\n repo: string,\n commentId: number,\n content: GithubReactionContent,\n ): Promise<void> {\n await this.request(\"POST\", `/repos/${owner}/${repo}/issues/comments/${commentId}/reactions`, {\n body: { content },\n });\n }\n\n async createIssueReaction(\n owner: string,\n repo: string,\n number: number,\n content: GithubReactionContent,\n ): Promise<void> {\n await this.request(\"POST\", `/repos/${owner}/${repo}/issues/${number}/reactions`, {\n body: { content },\n });\n }\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ConversationMessage, ConversationResponder, MessagingInfo } from "../../adapter.js";
|
|
2
|
+
import { type GithubMessagingBot } from "./bot.js";
|
|
3
|
+
import type { GithubEvent } from "./types.js";
|
|
4
|
+
export declare function createGithubAdapters(event: GithubEvent, bot: GithubMessagingBot): {
|
|
5
|
+
message: ConversationMessage;
|
|
6
|
+
responder: ConversationResponder;
|
|
7
|
+
platform: MessagingInfo;
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/adapters/github/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,mBAAmB,EACnB,qBAAqB,EACrB,aAAa,EACd,MAAM,kBAAkB,CAAC;AAS1B,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,UAAU,CAAC;AAElB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAiB9C,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,WAAW,EAClB,GAAG,EAAE,kBAAkB,GACtB;IACD,OAAO,EAAE,mBAAmB,CAAC;IAC7B,SAAS,EAAE,qBAAqB,CAAC;IACjC,QAAQ,EAAE,aAAa,CAAC;CACzB,CA8LA"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as log from "../../log.js";
|
|
2
|
+
import { resolveChatSessionKey } from "../../sessions/policy.js";
|
|
3
|
+
import { createChatResponseErrorReporter, formatToolArgs, splitText, } from "../shared.js";
|
|
4
|
+
import { formatGithubContinuation, GITHUB_MAX_COMMENT_LENGTH, } from "./bot.js";
|
|
5
|
+
import { parseGithubConversationId } from "./ids.js";
|
|
6
|
+
const GITHUB_FORMATTING_GUIDE = `## GitHub Formatting (GitHub Flavored Markdown)
|
|
7
|
+
Standard Markdown plus tables, task lists, and fenced code blocks.
|
|
8
|
+
Reference issues/PRs as #123 and users as @login (mentions notify people — use sparingly).`;
|
|
9
|
+
function formatToolResult(result) {
|
|
10
|
+
const argsFormatted = formatToolArgs(result.args);
|
|
11
|
+
const duration = (result.durationMs / 1000).toFixed(1);
|
|
12
|
+
let text = `**${result.isError ? "Error" : "Done"} ${result.toolName}**`;
|
|
13
|
+
if (result.label)
|
|
14
|
+
text += `: ${result.label}`;
|
|
15
|
+
text += ` (${duration}s)\n`;
|
|
16
|
+
if (argsFormatted)
|
|
17
|
+
text += `\`\`\`\n${argsFormatted}\n\`\`\`\n`;
|
|
18
|
+
text += `**Result:**\n\`\`\`\n${result.result}\n\`\`\``;
|
|
19
|
+
return text;
|
|
20
|
+
}
|
|
21
|
+
export function createGithubAdapters(event, bot) {
|
|
22
|
+
let commentId = null;
|
|
23
|
+
let accumulatedText = "";
|
|
24
|
+
let updatePromise = Promise.resolve();
|
|
25
|
+
const conversationId = event.conversationId;
|
|
26
|
+
const ref = parseGithubConversationId(conversationId);
|
|
27
|
+
const message = {
|
|
28
|
+
id: event.ts,
|
|
29
|
+
sessionKey: event.sessionKey ??
|
|
30
|
+
resolveChatSessionKey({
|
|
31
|
+
conversationId,
|
|
32
|
+
conversationKind: event.conversationKind,
|
|
33
|
+
messageId: event.ts,
|
|
34
|
+
persistentTopLevel: true,
|
|
35
|
+
threadTs: event.thread_ts,
|
|
36
|
+
}),
|
|
37
|
+
conversationKind: event.conversationKind,
|
|
38
|
+
userId: event.user,
|
|
39
|
+
userName: event.userName,
|
|
40
|
+
text: event.text,
|
|
41
|
+
attachments: event.attachments,
|
|
42
|
+
threadTs: event.thread_ts,
|
|
43
|
+
};
|
|
44
|
+
const platform = {
|
|
45
|
+
name: "github",
|
|
46
|
+
formattingGuide: `${GITHUB_FORMATTING_GUIDE}\n\n` +
|
|
47
|
+
`## Conversation context\n` +
|
|
48
|
+
`This conversation IS GitHub issue/PR ${ref.owner}/${ref.repo}#${ref.number}: the ` +
|
|
49
|
+
`first message in the history is its title and body, and the following messages are ` +
|
|
50
|
+
`its comments. When the user says "this issue", they mean #${ref.number}.`,
|
|
51
|
+
channels: [],
|
|
52
|
+
users: [],
|
|
53
|
+
diagnostics: {
|
|
54
|
+
showUsageSummary: false,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
const reportResponseError = createChatResponseErrorReporter(() => ({
|
|
58
|
+
platform: "github",
|
|
59
|
+
conversationId,
|
|
60
|
+
messageId: message.id,
|
|
61
|
+
sessionKey: message.sessionKey,
|
|
62
|
+
responseMessageId: commentId,
|
|
63
|
+
conversationKind: message.conversationKind,
|
|
64
|
+
}));
|
|
65
|
+
async function postOrUpdateResponse(displayText) {
|
|
66
|
+
if (commentId !== null) {
|
|
67
|
+
await bot.updateMessage(conversationId, String(commentId), displayText);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
commentId = await bot.postComment(ref, displayText);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function postSplitResponse(text) {
|
|
74
|
+
const [displayText, ...extraParts] = splitText(text, GITHUB_MAX_COMMENT_LENGTH, formatGithubContinuation);
|
|
75
|
+
await postOrUpdateResponse(displayText);
|
|
76
|
+
for (const part of extraParts) {
|
|
77
|
+
await bot.postComment(ref, part);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function queueGithubResponse(label, operation, work, extra) {
|
|
81
|
+
updatePromise = updatePromise.then(async () => {
|
|
82
|
+
try {
|
|
83
|
+
await work();
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
log.logWarning(`GitHub ${label} error`, err instanceof Error ? err.message : String(err));
|
|
87
|
+
reportResponseError(err, operation, extra());
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
await updatePromise;
|
|
91
|
+
}
|
|
92
|
+
// No appendResponseDelta/finishResponse: GitHub gets the finished response
|
|
93
|
+
// in one comment. Streaming would edit the comment on every flush — API
|
|
94
|
+
// churn and "edited" noise for readers who refresh rather than watch typing.
|
|
95
|
+
const responder = {
|
|
96
|
+
respond: async (text) => {
|
|
97
|
+
await queueGithubResponse("respond", "respond", async () => {
|
|
98
|
+
accumulatedText = accumulatedText ? `${accumulatedText}\n${text}` : text;
|
|
99
|
+
await postSplitResponse(accumulatedText);
|
|
100
|
+
if (commentId !== null) {
|
|
101
|
+
bot.logBotResponse(conversationId, text, String(commentId));
|
|
102
|
+
}
|
|
103
|
+
}, () => ({
|
|
104
|
+
phase: commentId ? "update" : "initial_post",
|
|
105
|
+
textLength: text.length,
|
|
106
|
+
accumulatedLength: accumulatedText.length,
|
|
107
|
+
}));
|
|
108
|
+
},
|
|
109
|
+
replaceResponse: async (text) => {
|
|
110
|
+
await queueGithubResponse("replaceResponse", "replace_response", async () => {
|
|
111
|
+
accumulatedText = text;
|
|
112
|
+
await postSplitResponse(accumulatedText);
|
|
113
|
+
}, () => ({
|
|
114
|
+
textLength: text.length,
|
|
115
|
+
hadExistingResponse: Boolean(commentId),
|
|
116
|
+
}));
|
|
117
|
+
},
|
|
118
|
+
respondDiagnostic: async (text, options) => {
|
|
119
|
+
await queueGithubResponse("respondDiagnostic", "respond_diagnostic", async () => {
|
|
120
|
+
const prefix = options?.style === "error" ? "**Error:** " : "";
|
|
121
|
+
for (const part of splitText(`${prefix}${text}`, GITHUB_MAX_COMMENT_LENGTH, formatGithubContinuation)) {
|
|
122
|
+
await bot.postComment(ref, part);
|
|
123
|
+
}
|
|
124
|
+
}, () => ({ textLength: text.length, style: options?.style }));
|
|
125
|
+
},
|
|
126
|
+
respondToolResult: async (result) => {
|
|
127
|
+
await responder.respondDiagnostic(formatToolResult(result));
|
|
128
|
+
},
|
|
129
|
+
// GitHub has no typing indicator; progress shows through comment edits.
|
|
130
|
+
setTyping: async () => { },
|
|
131
|
+
setWorking: async () => { },
|
|
132
|
+
uploadFile: async (filePath, title) => {
|
|
133
|
+
// The REST API cannot attach files to comments (uploads are a browser
|
|
134
|
+
// feature); leave a pointer instead of failing the run.
|
|
135
|
+
const name = title ?? filePath;
|
|
136
|
+
await queueGithubResponse("uploadFile", "respond_diagnostic", async () => {
|
|
137
|
+
await bot.postComment(ref, `*(file \`${name}\` was produced, but the GitHub adapter cannot attach files to comments)*`);
|
|
138
|
+
}, () => ({ filePath }));
|
|
139
|
+
},
|
|
140
|
+
react: async (emoji) => {
|
|
141
|
+
await bot.addReaction(conversationId, event.ts, emoji);
|
|
142
|
+
},
|
|
143
|
+
deleteResponse: async () => {
|
|
144
|
+
updatePromise = updatePromise.then(async () => {
|
|
145
|
+
if (commentId !== null) {
|
|
146
|
+
try {
|
|
147
|
+
await bot.deleteComment(ref, commentId);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Ignore errors
|
|
151
|
+
}
|
|
152
|
+
commentId = null;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
await updatePromise;
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
return { message, responder, platform };
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../../src/adapters/github/context.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ChatToolResult,\n ConversationMessage,\n ConversationResponder,\n MessagingInfo,\n} from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport { resolveChatSessionKey } from \"../../sessions/policy.js\";\nimport {\n createChatResponseErrorReporter,\n formatToolArgs,\n splitText,\n type ChatResponseErrorOperation,\n} from \"../shared.js\";\nimport {\n formatGithubContinuation,\n GITHUB_MAX_COMMENT_LENGTH,\n type GithubMessagingBot,\n} from \"./bot.js\";\nimport { parseGithubConversationId } from \"./ids.js\";\nimport type { GithubEvent } from \"./types.js\";\n\nconst GITHUB_FORMATTING_GUIDE = `## GitHub Formatting (GitHub Flavored Markdown)\nStandard Markdown plus tables, task lists, and fenced code blocks.\nReference issues/PRs as #123 and users as @login (mentions notify people — use sparingly).`;\n\nfunction formatToolResult(result: ChatToolResult): string {\n const argsFormatted = formatToolArgs(result.args);\n const duration = (result.durationMs / 1000).toFixed(1);\n let text = `**${result.isError ? \"Error\" : \"Done\"} ${result.toolName}**`;\n if (result.label) text += `: ${result.label}`;\n text += ` (${duration}s)\\n`;\n if (argsFormatted) text += `\\`\\`\\`\\n${argsFormatted}\\n\\`\\`\\`\\n`;\n text += `**Result:**\\n\\`\\`\\`\\n${result.result}\\n\\`\\`\\``;\n return text;\n}\n\nexport function createGithubAdapters(\n event: GithubEvent,\n bot: GithubMessagingBot,\n): {\n message: ConversationMessage;\n responder: ConversationResponder;\n platform: MessagingInfo;\n} {\n let commentId: number | null = null;\n let accumulatedText = \"\";\n let updatePromise = Promise.resolve();\n\n const conversationId = event.conversationId;\n const ref = parseGithubConversationId(conversationId);\n\n const message: ConversationMessage = {\n id: event.ts,\n sessionKey:\n event.sessionKey ??\n resolveChatSessionKey({\n conversationId,\n conversationKind: event.conversationKind,\n messageId: event.ts,\n persistentTopLevel: true,\n threadTs: event.thread_ts,\n }),\n conversationKind: event.conversationKind,\n userId: event.user,\n userName: event.userName,\n text: event.text,\n attachments: event.attachments,\n threadTs: event.thread_ts,\n };\n\n const platform: MessagingInfo = {\n name: \"github\",\n formattingGuide:\n `${GITHUB_FORMATTING_GUIDE}\\n\\n` +\n `## Conversation context\\n` +\n `This conversation IS GitHub issue/PR ${ref.owner}/${ref.repo}#${ref.number}: the ` +\n `first message in the history is its title and body, and the following messages are ` +\n `its comments. When the user says \"this issue\", they mean #${ref.number}.`,\n channels: [],\n users: [],\n diagnostics: {\n showUsageSummary: false,\n },\n };\n\n const reportResponseError = createChatResponseErrorReporter(() => ({\n platform: \"github\",\n conversationId,\n messageId: message.id,\n sessionKey: message.sessionKey,\n responseMessageId: commentId,\n conversationKind: message.conversationKind,\n }));\n\n async function postOrUpdateResponse(displayText: string): Promise<void> {\n if (commentId !== null) {\n await bot.updateMessage(conversationId, String(commentId), displayText);\n } else {\n commentId = await bot.postComment(ref, displayText);\n }\n }\n\n async function postSplitResponse(text: string): Promise<void> {\n const [displayText, ...extraParts] = splitText(\n text,\n GITHUB_MAX_COMMENT_LENGTH,\n formatGithubContinuation,\n );\n await postOrUpdateResponse(displayText);\n for (const part of extraParts) {\n await bot.postComment(ref, part);\n }\n }\n\n async function queueGithubResponse(\n label: string,\n operation: ChatResponseErrorOperation,\n work: () => Promise<void>,\n extra: () => Record<string, unknown>,\n ): Promise<void> {\n updatePromise = updatePromise.then(async () => {\n try {\n await work();\n } catch (err) {\n log.logWarning(`GitHub ${label} error`, err instanceof Error ? err.message : String(err));\n reportResponseError(err, operation, extra());\n }\n });\n await updatePromise;\n }\n\n // No appendResponseDelta/finishResponse: GitHub gets the finished response\n // in one comment. Streaming would edit the comment on every flush — API\n // churn and \"edited\" noise for readers who refresh rather than watch typing.\n const responder: ConversationResponder = {\n respond: async (text: string) => {\n await queueGithubResponse(\n \"respond\",\n \"respond\",\n async () => {\n accumulatedText = accumulatedText ? `${accumulatedText}\\n${text}` : text;\n await postSplitResponse(accumulatedText);\n if (commentId !== null) {\n bot.logBotResponse(conversationId, text, String(commentId));\n }\n },\n () => ({\n phase: commentId ? \"update\" : \"initial_post\",\n textLength: text.length,\n accumulatedLength: accumulatedText.length,\n }),\n );\n },\n\n replaceResponse: async (text: string) => {\n await queueGithubResponse(\n \"replaceResponse\",\n \"replace_response\",\n async () => {\n accumulatedText = text;\n await postSplitResponse(accumulatedText);\n },\n () => ({\n textLength: text.length,\n hadExistingResponse: Boolean(commentId),\n }),\n );\n },\n\n respondDiagnostic: async (text: string, options?: { style?: \"muted\" | \"error\" }) => {\n await queueGithubResponse(\n \"respondDiagnostic\",\n \"respond_diagnostic\",\n async () => {\n const prefix = options?.style === \"error\" ? \"**Error:** \" : \"\";\n for (const part of splitText(\n `${prefix}${text}`,\n GITHUB_MAX_COMMENT_LENGTH,\n formatGithubContinuation,\n )) {\n await bot.postComment(ref, part);\n }\n },\n () => ({ textLength: text.length, style: options?.style }),\n );\n },\n\n respondToolResult: async (result: ChatToolResult) => {\n await responder.respondDiagnostic(formatToolResult(result));\n },\n\n // GitHub has no typing indicator; progress shows through comment edits.\n setTyping: async () => {},\n\n setWorking: async () => {},\n\n uploadFile: async (filePath: string, title?: string) => {\n // The REST API cannot attach files to comments (uploads are a browser\n // feature); leave a pointer instead of failing the run.\n const name = title ?? filePath;\n await queueGithubResponse(\n \"uploadFile\",\n \"respond_diagnostic\",\n async () => {\n await bot.postComment(\n ref,\n `*(file \\`${name}\\` was produced, but the GitHub adapter cannot attach files to comments)*`,\n );\n },\n () => ({ filePath }),\n );\n },\n\n react: async (emoji: string) => {\n await bot.addReaction(conversationId, event.ts, emoji);\n },\n\n deleteResponse: async () => {\n updatePromise = updatePromise.then(async () => {\n if (commentId !== null) {\n try {\n await bot.deleteComment(ref, commentId);\n } catch {\n // Ignore errors\n }\n commentId = null;\n }\n });\n await updatePromise;\n },\n };\n\n return { message, responder, platform };\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { GithubConversationRef } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Conversation id for one issue/PR: `GH_<owner>_<repo>_<number>`.
|
|
4
|
+
*
|
|
5
|
+
* DESIGN.md originally proposed `gh:<owner>/<repo>#<number>`, but conversation
|
|
6
|
+
* ids are used verbatim as a single path segment (LAYOUT.md § Casing) and the
|
|
7
|
+
* conversation dir is bind-mounted with docker's `-v source:target` syntax in
|
|
8
|
+
* image mode — so the id can contain neither `/` nor `:`. `_` is the
|
|
9
|
+
* separator because it is the only inert character that also parses back
|
|
10
|
+
* unambiguously under GitHub's name grammar: owners allow `-` (so `-` would
|
|
11
|
+
* make the owner/repo boundary ambiguous) but never `_`, repos may contain
|
|
12
|
+
* `_` but the trailing number is pure digits — so the first `_` and the last
|
|
13
|
+
* `_` before the digits are always the real boundaries.
|
|
14
|
+
*
|
|
15
|
+
* Owner and repo are lowercased: GitHub names are case-insensitive (and this
|
|
16
|
+
* id has two spelling sources — the GITHUB_REPOS env var and API payloads),
|
|
17
|
+
* so per LAYOUT.md § Casing this is a mikan-derived slug, not a verbatim
|
|
18
|
+
* platform id, and derived slugs are lowercased to keep one issue from
|
|
19
|
+
* mapping to two conversation dirs on case-sensitive filesystems.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildGithubConversationId(ref: GithubConversationRef): string;
|
|
22
|
+
export declare function parseGithubConversationId(conversationId: string): GithubConversationRef;
|
|
23
|
+
/** Message ts used for the issue/PR body itself (comments use their comment id). */
|
|
24
|
+
export declare const GITHUB_ISSUE_BODY_TS = "issue";
|
|
25
|
+
//# sourceMappingURL=ids.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ids.d.ts","sourceRoot":"","sources":["../../../src/adapters/github/ids.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAExD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,qBAAqB,GAAG,MAAM,CAE5E;AAID,wBAAgB,yBAAyB,CAAC,cAAc,EAAE,MAAM,GAAG,qBAAqB,CAYvF;AAED,oFAAoF;AACpF,eAAO,MAAM,oBAAoB,UAAU,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation id for one issue/PR: `GH_<owner>_<repo>_<number>`.
|
|
3
|
+
*
|
|
4
|
+
* DESIGN.md originally proposed `gh:<owner>/<repo>#<number>`, but conversation
|
|
5
|
+
* ids are used verbatim as a single path segment (LAYOUT.md § Casing) and the
|
|
6
|
+
* conversation dir is bind-mounted with docker's `-v source:target` syntax in
|
|
7
|
+
* image mode — so the id can contain neither `/` nor `:`. `_` is the
|
|
8
|
+
* separator because it is the only inert character that also parses back
|
|
9
|
+
* unambiguously under GitHub's name grammar: owners allow `-` (so `-` would
|
|
10
|
+
* make the owner/repo boundary ambiguous) but never `_`, repos may contain
|
|
11
|
+
* `_` but the trailing number is pure digits — so the first `_` and the last
|
|
12
|
+
* `_` before the digits are always the real boundaries.
|
|
13
|
+
*
|
|
14
|
+
* Owner and repo are lowercased: GitHub names are case-insensitive (and this
|
|
15
|
+
* id has two spelling sources — the GITHUB_REPOS env var and API payloads),
|
|
16
|
+
* so per LAYOUT.md § Casing this is a mikan-derived slug, not a verbatim
|
|
17
|
+
* platform id, and derived slugs are lowercased to keep one issue from
|
|
18
|
+
* mapping to two conversation dirs on case-sensitive filesystems.
|
|
19
|
+
*/
|
|
20
|
+
export function buildGithubConversationId(ref) {
|
|
21
|
+
return `GH_${ref.owner.toLowerCase()}_${ref.repo.toLowerCase()}_${ref.number}`;
|
|
22
|
+
}
|
|
23
|
+
const CONVERSATION_ID_PATTERN = /^GH_([A-Za-z0-9-]+)_(.+)_(\d+)$/;
|
|
24
|
+
export function parseGithubConversationId(conversationId) {
|
|
25
|
+
// The greedy repo group splits at the last `_` followed by digits-to-end,
|
|
26
|
+
// which is always the number boundary (repos may contain `_` themselves).
|
|
27
|
+
const match = CONVERSATION_ID_PATTERN.exec(conversationId);
|
|
28
|
+
if (!match) {
|
|
29
|
+
throw new Error(`Not a GitHub conversation id: ${conversationId}`);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
owner: match[1].toLowerCase(),
|
|
33
|
+
repo: match[2].toLowerCase(),
|
|
34
|
+
number: Number(match[3]),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Message ts used for the issue/PR body itself (comments use their comment id). */
|
|
38
|
+
export const GITHUB_ISSUE_BODY_TS = "issue";
|
|
39
|
+
//# sourceMappingURL=ids.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ids.js","sourceRoot":"","sources":["../../../src/adapters/github/ids.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,yBAAyB,CAAC,GAA0B;IAClE,OAAO,MAAM,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;AACjF,CAAC;AAED,MAAM,uBAAuB,GAAG,iCAAiC,CAAC;AAElE,MAAM,UAAU,yBAAyB,CAAC,cAAsB;IAC9D,0EAA0E;IAC1E,0EAA0E;IAC1E,MAAM,KAAK,GAAG,uBAAuB,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC3D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,iCAAiC,cAAc,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE;QAC7B,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE;QAC5B,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;KACzB,CAAC;AACJ,CAAC;AAED,oFAAoF;AACpF,MAAM,CAAC,MAAM,oBAAoB,GAAG,OAAO,CAAC","sourcesContent":["import type { GithubConversationRef } from \"./types.js\";\n\n/**\n * Conversation id for one issue/PR: `GH_<owner>_<repo>_<number>`.\n *\n * DESIGN.md originally proposed `gh:<owner>/<repo>#<number>`, but conversation\n * ids are used verbatim as a single path segment (LAYOUT.md § Casing) and the\n * conversation dir is bind-mounted with docker's `-v source:target` syntax in\n * image mode — so the id can contain neither `/` nor `:`. `_` is the\n * separator because it is the only inert character that also parses back\n * unambiguously under GitHub's name grammar: owners allow `-` (so `-` would\n * make the owner/repo boundary ambiguous) but never `_`, repos may contain\n * `_` but the trailing number is pure digits — so the first `_` and the last\n * `_` before the digits are always the real boundaries.\n *\n * Owner and repo are lowercased: GitHub names are case-insensitive (and this\n * id has two spelling sources — the GITHUB_REPOS env var and API payloads),\n * so per LAYOUT.md § Casing this is a mikan-derived slug, not a verbatim\n * platform id, and derived slugs are lowercased to keep one issue from\n * mapping to two conversation dirs on case-sensitive filesystems.\n */\nexport function buildGithubConversationId(ref: GithubConversationRef): string {\n return `GH_${ref.owner.toLowerCase()}_${ref.repo.toLowerCase()}_${ref.number}`;\n}\n\nconst CONVERSATION_ID_PATTERN = /^GH_([A-Za-z0-9-]+)_(.+)_(\\d+)$/;\n\nexport function parseGithubConversationId(conversationId: string): GithubConversationRef {\n // The greedy repo group splits at the last `_` followed by digits-to-end,\n // which is always the number boundary (repos may contain `_` themselves).\n const match = CONVERSATION_ID_PATTERN.exec(conversationId);\n if (!match) {\n throw new Error(`Not a GitHub conversation id: ${conversationId}`);\n }\n return {\n owner: match[1].toLowerCase(),\n repo: match[2].toLowerCase(),\n number: Number(match[3]),\n };\n}\n\n/** Message ts used for the issue/PR body itself (comments use their comment id). */\nexport const GITHUB_ISSUE_BODY_TS = \"issue\";\n"]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ConversationEvent } from "../../adapter.js";
|
|
2
|
+
export interface GithubEvent extends ConversationEvent {
|
|
3
|
+
type: "message" | "issue";
|
|
4
|
+
userName?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface GithubBotConfig {
|
|
7
|
+
appId: string;
|
|
8
|
+
/** PEM-encoded GitHub App private key. */
|
|
9
|
+
privateKey: string;
|
|
10
|
+
installationId: string;
|
|
11
|
+
/** `owner/repo` entries to watch; empty = all installation repositories. */
|
|
12
|
+
repos: string[];
|
|
13
|
+
pollIntervalMs: number;
|
|
14
|
+
workingDir: string;
|
|
15
|
+
/**
|
|
16
|
+
* Where the poll watermark state (baseline + seen ids) is persisted. Lives
|
|
17
|
+
* under the host-only state dir: the workspace dir can be mounted into
|
|
18
|
+
* sandboxes, and sandboxed code must not be able to clear dedup state and
|
|
19
|
+
* re-trigger runs.
|
|
20
|
+
*/
|
|
21
|
+
syncStatePath: string;
|
|
22
|
+
}
|
|
23
|
+
/** Persisted per-repo poll watermark (DESIGN.md § Event source). */
|
|
24
|
+
interface GithubRepoSyncState {
|
|
25
|
+
/** Comments/issues created before this ISO instant never trigger. */
|
|
26
|
+
baseline: string;
|
|
27
|
+
/** Newest updated_at seen; polls fetch from (cursor - overlap). */
|
|
28
|
+
cursor: string;
|
|
29
|
+
seenComments: number[];
|
|
30
|
+
seenIssues: number[];
|
|
31
|
+
}
|
|
32
|
+
export interface GithubSyncState {
|
|
33
|
+
repos: Record<string, GithubRepoSyncState>;
|
|
34
|
+
}
|
|
35
|
+
export interface GithubRepoRef {
|
|
36
|
+
owner: string;
|
|
37
|
+
repo: string;
|
|
38
|
+
}
|
|
39
|
+
export interface GithubConversationRef extends GithubRepoRef {
|
|
40
|
+
number: number;
|
|
41
|
+
}
|
|
42
|
+
interface GithubUser {
|
|
43
|
+
login: string;
|
|
44
|
+
type: string;
|
|
45
|
+
}
|
|
46
|
+
export interface GithubIssue {
|
|
47
|
+
id: number;
|
|
48
|
+
number: number;
|
|
49
|
+
title: string;
|
|
50
|
+
body: string | null;
|
|
51
|
+
user: GithubUser;
|
|
52
|
+
created_at: string;
|
|
53
|
+
updated_at: string;
|
|
54
|
+
pull_request?: object;
|
|
55
|
+
}
|
|
56
|
+
export interface GithubIssueComment {
|
|
57
|
+
id: number;
|
|
58
|
+
body: string;
|
|
59
|
+
user: GithubUser;
|
|
60
|
+
created_at: string;
|
|
61
|
+
updated_at: string;
|
|
62
|
+
/** API URL of the parent issue, e.g. …/repos/o/r/issues/42 */
|
|
63
|
+
issue_url: string;
|
|
64
|
+
}
|
|
65
|
+
export interface GithubRepository {
|
|
66
|
+
name: string;
|
|
67
|
+
owner: GithubUser;
|
|
68
|
+
}
|
|
69
|
+
export type GithubReactionContent = "+1" | "-1" | "laugh" | "confused" | "heart" | "hooray" | "rocket" | "eyes";
|
|
70
|
+
export interface GithubClientOptions {
|
|
71
|
+
appId: string;
|
|
72
|
+
privateKey: string;
|
|
73
|
+
installationId: string;
|
|
74
|
+
/** Override the API root for tests; defaults to https://api.github.com */
|
|
75
|
+
baseUrl?: string;
|
|
76
|
+
fetchImpl?: typeof fetch;
|
|
77
|
+
}
|
|
78
|
+
export {};
|
|
79
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/adapters/github/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,MAAM,WAAW,WAAY,SAAQ,iBAAiB;IACpD,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,oEAAoE;AACpE,UAAU,mBAAmB;IAC3B,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,MAAM,EAAE,MAAM,CAAC;CAChB;AAID,UAAU,UAAU;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,MAAM,qBAAqB,GAC7B,IAAI,GACJ,IAAI,GACJ,OAAO,GACP,UAAU,GACV,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,MAAM,CAAC;AAEX,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/adapters/github/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ConversationEvent } from \"../../adapter.js\";\n\nexport interface GithubEvent extends ConversationEvent {\n type: \"message\" | \"issue\";\n userName?: string;\n}\n\nexport interface GithubBotConfig {\n appId: string;\n /** PEM-encoded GitHub App private key. */\n privateKey: string;\n installationId: string;\n /** `owner/repo` entries to watch; empty = all installation repositories. */\n repos: string[];\n pollIntervalMs: number;\n workingDir: string;\n /**\n * Where the poll watermark state (baseline + seen ids) is persisted. Lives\n * under the host-only state dir: the workspace dir can be mounted into\n * sandboxes, and sandboxed code must not be able to clear dedup state and\n * re-trigger runs.\n */\n syncStatePath: string;\n}\n\n/** Persisted per-repo poll watermark (DESIGN.md § Event source). */\ninterface GithubRepoSyncState {\n /** Comments/issues created before this ISO instant never trigger. */\n baseline: string;\n /** Newest updated_at seen; polls fetch from (cursor - overlap). */\n cursor: string;\n seenComments: number[];\n seenIssues: number[];\n}\n\nexport interface GithubSyncState {\n repos: Record<string, GithubRepoSyncState>;\n}\n\nexport interface GithubRepoRef {\n owner: string;\n repo: string;\n}\n\nexport interface GithubConversationRef extends GithubRepoRef {\n number: number;\n}\n\n// ── GitHub REST payload shapes (only the fields mikan reads) ─────────────────\n\ninterface GithubUser {\n login: string;\n type: string;\n}\n\nexport interface GithubIssue {\n id: number;\n number: number;\n title: string;\n body: string | null;\n user: GithubUser;\n created_at: string;\n updated_at: string;\n pull_request?: object;\n}\n\nexport interface GithubIssueComment {\n id: number;\n body: string;\n user: GithubUser;\n created_at: string;\n updated_at: string;\n /** API URL of the parent issue, e.g. …/repos/o/r/issues/42 */\n issue_url: string;\n}\n\nexport interface GithubRepository {\n name: string;\n owner: GithubUser;\n}\n\nexport type GithubReactionContent =\n | \"+1\"\n | \"-1\"\n | \"laugh\"\n | \"confused\"\n | \"heart\"\n | \"hooray\"\n | \"rocket\"\n | \"eyes\";\n\nexport interface GithubClientOptions {\n appId: string;\n privateKey: string;\n installationId: string;\n /** Override the API root for tests; defaults to https://api.github.com */\n baseUrl?: string;\n fetchImpl?: typeof fetch;\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface ResolvedGitSource {
|
|
2
|
+
/** Local directory holding the extension (clone root or a subpath within). */
|
|
3
|
+
dir: string;
|
|
4
|
+
/** Remove the temporary clone. */
|
|
5
|
+
cleanup: () => void;
|
|
6
|
+
}
|
|
7
|
+
interface GitSpec {
|
|
8
|
+
url: string;
|
|
9
|
+
subpath?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Recognize a git source and split off an optional `#subpath`:
|
|
13
|
+
* https://host/owner/repo.git#dir/ext → { url, subpath }
|
|
14
|
+
* git@host:owner/repo.git → { url }
|
|
15
|
+
* github:owner/repo → expands to https clone URL
|
|
16
|
+
* Returns undefined for anything that looks like a local path.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseGitSource(source: string): GitSpec | undefined;
|
|
19
|
+
/** Clone a git source (shallow) and resolve the extension directory within it. */
|
|
20
|
+
export declare function resolveGitSource(spec: GitSpec): ResolvedGitSource;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=ext-git.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ext-git.d.ts","sourceRoot":"","sources":["../../src/cli/ext-git.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,iBAAiB;IAChC,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAC;IACZ,kCAAkC;IAClC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,UAAU,OAAO;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAkBlE;AAQD,kFAAkF;AAClF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,iBAAiB,CAuBjE"}
|