@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
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,37 @@ any release.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [1.0.0-beta.8]
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Add a GitHub messaging adapter where each issue or pull request is an independent conversation.
|
|
17
|
+
- Add GitHub App authentication, mention and participation triggers, persisted polling cursors, and downtime replay.
|
|
18
|
+
- Add GitHub adapter deployment configuration and documentation.
|
|
19
|
+
|
|
20
|
+
### Tests
|
|
21
|
+
|
|
22
|
+
- Add coverage for GitHub polling, authentication, conversation context, triggers, and responses.
|
|
23
|
+
|
|
24
|
+
## [1.0.0-beta.7]
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Add TypeScript and npm-package extensions with `package.json` entrypoints and runtime dependencies.
|
|
29
|
+
- Add `mikan ext install`, `validate`, `list`, and `remove` commands.
|
|
30
|
+
- Add extension installation from Git sources and repository subpaths.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Treat reinstalling an extension as an update while preserving its data directory.
|
|
35
|
+
- Use `package.json` as the primary extension metadata source with `manifest.json` as a fallback.
|
|
36
|
+
- Convert the agent project manager example to TypeScript with a typed extension API.
|
|
37
|
+
- Add the Slack `reactions:write` scope and missing slash commands to the manifests and setup guides.
|
|
38
|
+
|
|
39
|
+
### Tests
|
|
40
|
+
|
|
41
|
+
- Add coverage for TypeScript loading, package metadata, extension CLI operations, and Git installation.
|
|
42
|
+
|
|
12
43
|
## [1.0.0-beta.6]
|
|
13
44
|
|
|
14
45
|
### Added
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ConversationEvent, MessagingBot, MessagingEventHandler, MessagingInfo } from "../../adapter.js";
|
|
2
|
+
import { GithubClient } from "./client.js";
|
|
3
|
+
import type { GithubBotConfig, GithubConversationRef } from "./types.js";
|
|
4
|
+
/** GitHub comment bodies cap at 65536 chars; leave headroom for markup. */
|
|
5
|
+
export declare const GITHUB_MAX_COMMENT_LENGTH = 60000;
|
|
6
|
+
export declare const formatGithubContinuation: (partNum: number) => string;
|
|
7
|
+
export declare class GithubMessagingBot implements MessagingBot {
|
|
8
|
+
private readonly client;
|
|
9
|
+
private readonly handler;
|
|
10
|
+
private readonly config;
|
|
11
|
+
private appSlug;
|
|
12
|
+
private watchedRepos;
|
|
13
|
+
private queues;
|
|
14
|
+
private repoState;
|
|
15
|
+
private pollInFlight;
|
|
16
|
+
constructor(handler: MessagingEventHandler, config: GithubBotConfig, client?: GithubClient);
|
|
17
|
+
start(): Promise<void>;
|
|
18
|
+
postMessage(channel: string, text: string): Promise<string>;
|
|
19
|
+
updateMessage(channel: string, ts: string, text: string): Promise<void>;
|
|
20
|
+
addReaction(channel: string, messageTs: string, emoji: string): Promise<void>;
|
|
21
|
+
enqueueEvent(event: ConversationEvent): boolean;
|
|
22
|
+
getMessagingInfo(): MessagingInfo;
|
|
23
|
+
postComment(ref: GithubConversationRef, text: string): Promise<number>;
|
|
24
|
+
deleteComment(ref: GithubConversationRef, commentId: number): Promise<void>;
|
|
25
|
+
logToFile(conversationId: string, entry: object): void;
|
|
26
|
+
logBotResponse(conversationId: string, text: string, ts: string): void;
|
|
27
|
+
private getQueue;
|
|
28
|
+
/** One poll tick over every watched repo. Public for tests. */
|
|
29
|
+
poll(): Promise<void>;
|
|
30
|
+
private pollRepo;
|
|
31
|
+
private loadSyncState;
|
|
32
|
+
private persistSyncState;
|
|
33
|
+
/** Fresh regex per call: the `g` flag makes `test`/`replace` stateful. */
|
|
34
|
+
private mentionPattern;
|
|
35
|
+
private isMentioned;
|
|
36
|
+
private stripMention;
|
|
37
|
+
private isParticipating;
|
|
38
|
+
private handleIncoming;
|
|
39
|
+
private logIssueContext;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=bot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../../src/adapters/github/bot.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,iBAAiB,EACjB,YAAY,EACZ,qBAAqB,EACrB,aAAa,EACd,MAAM,kBAAkB,CAAC;AAe1B,OAAO,EAAE,YAAY,EAAuB,MAAM,aAAa,CAAC;AAOhE,OAAO,KAAK,EACV,eAAe,EACf,qBAAqB,EAMtB,MAAM,YAAY,CAAC;AAkCpB,2EAA2E;AAC3E,eAAO,MAAM,yBAAyB,QAAQ,CAAC;AAE/C,eAAO,MAAM,wBAAwB,YAAa,MAAM,KAAG,MAAoC,CAAC;AA2DhG,qBAAa,kBAAmB,YAAW,YAAY;IACrD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,MAAM,CAA0C;IACxD,OAAO,CAAC,SAAS,CAAoC;IACrD,OAAO,CAAC,YAAY,CAAS;IAE7B,YAAY,OAAO,EAAE,qBAAqB,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,YAAY,EAUzF;IAMK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CA0C3B;IAEK,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQhE;IAEK,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5E;IAEK,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAalF;IAED,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAe9C;IAED,gBAAgB,IAAI,aAAa,CAUhC;IAMK,WAAW,CAAC,GAAG,EAAE,qBAAqB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK3E;IAEK,aAAa,CAAC,GAAG,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhF;IAED,SAAS,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAErD;IAED,cAAc,CAAC,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAErE;IAMD,OAAO,CAAC,QAAQ;IAShB,+DAA+D;IACzD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAqB1B;YAEa,QAAQ;IA2DtB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,gBAAgB;IAkBxB,0EAA0E;IAC1E,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,eAAe;YAIT,cAAc;YA2Ed,eAAe;CA+B9B"}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { Type } from "@sinclair/typebox";
|
|
4
|
+
import * as log from "../../log.js";
|
|
5
|
+
import { ensureDirExists, readJsonSchemaFileIfExists } from "../../utils/file-guards.js";
|
|
6
|
+
import { atomicWritePrivateFile } from "../../utils/fs-atomic.js";
|
|
7
|
+
import { resolveChatSessionKey } from "../../sessions/policy.js";
|
|
8
|
+
import { formatNothingRunning } from "../../platform-messages.js";
|
|
9
|
+
import { appendBotResponseLog, appendChannelLog, MessagingEventQueue, resolveStopTarget, splitText, withRetry, } from "../shared.js";
|
|
10
|
+
import { processMessageIntake } from "../intake.js";
|
|
11
|
+
import { GithubClient, githubIsRateLimited } from "./client.js";
|
|
12
|
+
import { createGithubAdapters } from "./context.js";
|
|
13
|
+
import { buildGithubConversationId, GITHUB_ISSUE_BODY_TS, parseGithubConversationId, } from "./ids.js";
|
|
14
|
+
const SyncStateSchema = Type.Object({
|
|
15
|
+
repos: Type.Record(Type.String(), Type.Object({
|
|
16
|
+
baseline: Type.String(),
|
|
17
|
+
cursor: Type.String(),
|
|
18
|
+
seenComments: Type.Array(Type.Number()),
|
|
19
|
+
seenIssues: Type.Array(Type.Number()),
|
|
20
|
+
})),
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Re-fetch this far behind the cursor each poll. Dedup is by id (the
|
|
24
|
+
* watermark), so the overlap only costs bandwidth and closes the boundary
|
|
25
|
+
* races of a bare `since` timestamp (DESIGN.md § Event source).
|
|
26
|
+
*/
|
|
27
|
+
const POLL_OVERLAP_MS = 5 * 60 * 1000;
|
|
28
|
+
const MAX_SEEN_IDS = 5000;
|
|
29
|
+
const githubRetry = (fn) => withRetry(fn, { isRateLimited: githubIsRateLimited });
|
|
30
|
+
/** GitHub comment bodies cap at 65536 chars; leave headroom for markup. */
|
|
31
|
+
export const GITHUB_MAX_COMMENT_LENGTH = 60000;
|
|
32
|
+
export const formatGithubContinuation = (partNum) => `*(continued ${partNum})*`;
|
|
33
|
+
/** Slack-style emoji short names → GitHub reaction content values. */
|
|
34
|
+
const GITHUB_REACTIONS = {
|
|
35
|
+
"+1": "+1",
|
|
36
|
+
thumbsup: "+1",
|
|
37
|
+
"-1": "-1",
|
|
38
|
+
thumbsdown: "-1",
|
|
39
|
+
laugh: "laugh",
|
|
40
|
+
smile: "laugh",
|
|
41
|
+
confused: "confused",
|
|
42
|
+
heart: "heart",
|
|
43
|
+
hooray: "hooray",
|
|
44
|
+
tada: "hooray",
|
|
45
|
+
rocket: "rocket",
|
|
46
|
+
eyes: "eyes",
|
|
47
|
+
};
|
|
48
|
+
// Repo refs are lowercased at every entry point (env config here, the
|
|
49
|
+
// installation API in start()) so conversation ids and sync-state keys never
|
|
50
|
+
// depend on how a repo's name was spelled — GitHub is case-insensitive.
|
|
51
|
+
function parseRepoList(repos) {
|
|
52
|
+
return repos.map((entry) => {
|
|
53
|
+
const [owner, repo, ...rest] = entry.split("/");
|
|
54
|
+
if (!owner || !repo || rest.length > 0) {
|
|
55
|
+
throw new Error(`Invalid GITHUB_REPOS entry (expected owner/repo): ${entry}`);
|
|
56
|
+
}
|
|
57
|
+
return { owner: owner.toLowerCase(), repo: repo.toLowerCase() };
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** Sets iterate in insertion order; drop the oldest ids once over the cap. */
|
|
61
|
+
function pruneSeen(seen) {
|
|
62
|
+
if (seen.size <= MAX_SEEN_IDS)
|
|
63
|
+
return;
|
|
64
|
+
let toDrop = seen.size - MAX_SEEN_IDS / 2;
|
|
65
|
+
for (const id of seen) {
|
|
66
|
+
if (toDrop-- <= 0)
|
|
67
|
+
break;
|
|
68
|
+
seen.delete(id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Issue number from a comment's `issue_url` (…/repos/o/r/issues/42). */
|
|
72
|
+
function issueNumberFromUrl(issueUrl) {
|
|
73
|
+
const match = /\/issues\/(\d+)$/.exec(issueUrl);
|
|
74
|
+
if (!match) {
|
|
75
|
+
throw new Error(`Cannot parse issue number from ${issueUrl}`);
|
|
76
|
+
}
|
|
77
|
+
return Number(match[1]);
|
|
78
|
+
}
|
|
79
|
+
export class GithubMessagingBot {
|
|
80
|
+
constructor(handler, config, client) {
|
|
81
|
+
this.appSlug = null;
|
|
82
|
+
this.watchedRepos = [];
|
|
83
|
+
this.queues = new Map();
|
|
84
|
+
this.repoState = new Map();
|
|
85
|
+
this.pollInFlight = false;
|
|
86
|
+
this.handler = handler;
|
|
87
|
+
this.config = config;
|
|
88
|
+
this.client =
|
|
89
|
+
client ??
|
|
90
|
+
new GithubClient({
|
|
91
|
+
appId: config.appId,
|
|
92
|
+
privateKey: config.privateKey,
|
|
93
|
+
installationId: config.installationId,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// ==========================================================================
|
|
97
|
+
// Public API (implements MessagingBot)
|
|
98
|
+
// ==========================================================================
|
|
99
|
+
async start() {
|
|
100
|
+
this.appSlug = await this.client.getAppSlug();
|
|
101
|
+
this.watchedRepos =
|
|
102
|
+
this.config.repos.length > 0
|
|
103
|
+
? parseRepoList(this.config.repos)
|
|
104
|
+
: (await this.client.listInstallationRepositories()).map((repository) => ({
|
|
105
|
+
owner: repository.owner.login.toLowerCase(),
|
|
106
|
+
repo: repository.name.toLowerCase(),
|
|
107
|
+
}));
|
|
108
|
+
if (this.watchedRepos.length === 0) {
|
|
109
|
+
log.logWarning("GitHub: installation has no repositories; nothing to poll");
|
|
110
|
+
}
|
|
111
|
+
// Restore persisted watermarks; a repo watched for the first time gets a
|
|
112
|
+
// baseline of "now" so history never triggers (DESIGN.md § Event source).
|
|
113
|
+
const persisted = this.loadSyncState();
|
|
114
|
+
const now = new Date().toISOString();
|
|
115
|
+
for (const repo of this.watchedRepos) {
|
|
116
|
+
const repoKey = `${repo.owner}/${repo.repo}`;
|
|
117
|
+
const saved = persisted?.repos[repoKey];
|
|
118
|
+
this.repoState.set(repoKey, saved
|
|
119
|
+
? {
|
|
120
|
+
baseline: saved.baseline,
|
|
121
|
+
cursor: saved.cursor,
|
|
122
|
+
seenComments: new Set(saved.seenComments),
|
|
123
|
+
seenIssues: new Set(saved.seenIssues),
|
|
124
|
+
}
|
|
125
|
+
: { baseline: now, cursor: now, seenComments: new Set(), seenIssues: new Set() });
|
|
126
|
+
}
|
|
127
|
+
this.persistSyncState();
|
|
128
|
+
setInterval(() => {
|
|
129
|
+
void this.poll();
|
|
130
|
+
}, this.config.pollIntervalMs).unref();
|
|
131
|
+
log.logConnected("GitHub");
|
|
132
|
+
log.logInfo(`GitHub bot started as @${this.appSlug}[bot], polling ${this.watchedRepos.length} repo(s) every ${Math.round(this.config.pollIntervalMs / 1000)}s`);
|
|
133
|
+
}
|
|
134
|
+
async postMessage(channel, text) {
|
|
135
|
+
const ref = parseGithubConversationId(channel);
|
|
136
|
+
const parts = splitText(text, GITHUB_MAX_COMMENT_LENGTH, formatGithubContinuation);
|
|
137
|
+
const firstId = await this.postComment(ref, parts[0]);
|
|
138
|
+
for (const part of parts.slice(1)) {
|
|
139
|
+
await this.postComment(ref, part);
|
|
140
|
+
}
|
|
141
|
+
return String(firstId);
|
|
142
|
+
}
|
|
143
|
+
async updateMessage(channel, ts, text) {
|
|
144
|
+
const ref = parseGithubConversationId(channel);
|
|
145
|
+
await githubRetry(() => this.client.updateIssueComment(ref.owner, ref.repo, Number(ts), text));
|
|
146
|
+
}
|
|
147
|
+
async addReaction(channel, messageTs, emoji) {
|
|
148
|
+
const ref = parseGithubConversationId(channel);
|
|
149
|
+
const content = GITHUB_REACTIONS[emoji.replace(/^:|:$/g, "")];
|
|
150
|
+
if (!content) {
|
|
151
|
+
throw new Error(`GitHub does not support reaction '${emoji}' (supported: ${Object.keys(GITHUB_REACTIONS).join(", ")})`);
|
|
152
|
+
}
|
|
153
|
+
await githubRetry(() => messageTs === GITHUB_ISSUE_BODY_TS
|
|
154
|
+
? this.client.createIssueReaction(ref.owner, ref.repo, ref.number, content)
|
|
155
|
+
: this.client.createCommentReaction(ref.owner, ref.repo, Number(messageTs), content));
|
|
156
|
+
}
|
|
157
|
+
enqueueEvent(event) {
|
|
158
|
+
const conversationId = event.conversationId;
|
|
159
|
+
const queue = this.getQueue(conversationId);
|
|
160
|
+
if (queue.size() >= 5) {
|
|
161
|
+
log.logWarning(`Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`);
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);
|
|
165
|
+
queue.enqueue(() => {
|
|
166
|
+
const context = createGithubAdapters(event, this);
|
|
167
|
+
return this.handler.handleEvent(event, this, context);
|
|
168
|
+
});
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
getMessagingInfo() {
|
|
172
|
+
return {
|
|
173
|
+
name: "github",
|
|
174
|
+
formattingGuide: "## GitHub Formatting (GitHub Flavored Markdown)\n" +
|
|
175
|
+
"Standard Markdown plus tables, task lists, and ```suggestion blocks.\n" +
|
|
176
|
+
"Reference issues/PRs as #123 and users as @login.",
|
|
177
|
+
channels: [],
|
|
178
|
+
users: [],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// ==========================================================================
|
|
182
|
+
// Internal helpers (used by context.ts)
|
|
183
|
+
// ==========================================================================
|
|
184
|
+
async postComment(ref, text) {
|
|
185
|
+
const comment = await githubRetry(() => this.client.createIssueComment(ref.owner, ref.repo, ref.number, text));
|
|
186
|
+
return comment.id;
|
|
187
|
+
}
|
|
188
|
+
async deleteComment(ref, commentId) {
|
|
189
|
+
await githubRetry(() => this.client.deleteIssueComment(ref.owner, ref.repo, commentId));
|
|
190
|
+
}
|
|
191
|
+
logToFile(conversationId, entry) {
|
|
192
|
+
appendChannelLog(this.config.workingDir, conversationId, entry);
|
|
193
|
+
}
|
|
194
|
+
logBotResponse(conversationId, text, ts) {
|
|
195
|
+
appendBotResponseLog(this.config.workingDir, conversationId, text, ts);
|
|
196
|
+
}
|
|
197
|
+
// ==========================================================================
|
|
198
|
+
// Private - polling
|
|
199
|
+
// ==========================================================================
|
|
200
|
+
getQueue(conversationId) {
|
|
201
|
+
let queue = this.queues.get(conversationId);
|
|
202
|
+
if (!queue) {
|
|
203
|
+
queue = new MessagingEventQueue("GitHub");
|
|
204
|
+
this.queues.set(conversationId, queue);
|
|
205
|
+
}
|
|
206
|
+
return queue;
|
|
207
|
+
}
|
|
208
|
+
/** One poll tick over every watched repo. Public for tests. */
|
|
209
|
+
async poll() {
|
|
210
|
+
if (this.pollInFlight)
|
|
211
|
+
return;
|
|
212
|
+
this.pollInFlight = true;
|
|
213
|
+
try {
|
|
214
|
+
let changed = false;
|
|
215
|
+
for (const repo of this.watchedRepos) {
|
|
216
|
+
try {
|
|
217
|
+
changed = (await this.pollRepo(repo)) || changed;
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
log.logWarning(`GitHub poll failed for ${repo.owner}/${repo.repo}`, err instanceof Error ? err.message : String(err));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (changed) {
|
|
224
|
+
this.persistSyncState();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
this.pollInFlight = false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async pollRepo(repo) {
|
|
232
|
+
const repoKey = `${repo.owner}/${repo.repo}`;
|
|
233
|
+
const state = this.repoState.get(repoKey);
|
|
234
|
+
if (!state)
|
|
235
|
+
return false;
|
|
236
|
+
const since = new Date(Date.parse(state.cursor) - POLL_OVERLAP_MS).toISOString();
|
|
237
|
+
let changed = false;
|
|
238
|
+
// Comment activity also bumps the parent issue's updated_at, so the issues
|
|
239
|
+
// list mostly echoes the comments list; only issues *created* since the
|
|
240
|
+
// baseline are new conversations (their body is the first message).
|
|
241
|
+
const issues = (await this.client.listIssuesSince(repo.owner, repo.repo, since)) ?? [];
|
|
242
|
+
for (const issue of issues) {
|
|
243
|
+
if (issue.updated_at > state.cursor) {
|
|
244
|
+
state.cursor = issue.updated_at;
|
|
245
|
+
changed = true;
|
|
246
|
+
}
|
|
247
|
+
// Edits re-surface old items with a fresh updated_at; the id watermark
|
|
248
|
+
// plus the creation baseline decide what actually triggers.
|
|
249
|
+
if (issue.created_at < state.baseline || state.seenIssues.has(issue.id))
|
|
250
|
+
continue;
|
|
251
|
+
state.seenIssues.add(issue.id);
|
|
252
|
+
changed = true;
|
|
253
|
+
if (issue.user.type === "Bot")
|
|
254
|
+
continue;
|
|
255
|
+
await this.handleIncoming({
|
|
256
|
+
ref: { ...repo, number: issue.number },
|
|
257
|
+
ts: GITHUB_ISSUE_BODY_TS,
|
|
258
|
+
user: issue.user.login,
|
|
259
|
+
text: `# ${issue.title}\n\n${issue.body ?? ""}`.trim(),
|
|
260
|
+
createdAt: issue.created_at,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const comments = (await this.client.listIssueCommentsSince(repo.owner, repo.repo, since)) ?? [];
|
|
264
|
+
for (const comment of comments) {
|
|
265
|
+
if (comment.updated_at > state.cursor) {
|
|
266
|
+
state.cursor = comment.updated_at;
|
|
267
|
+
changed = true;
|
|
268
|
+
}
|
|
269
|
+
if (comment.created_at < state.baseline || state.seenComments.has(comment.id))
|
|
270
|
+
continue;
|
|
271
|
+
state.seenComments.add(comment.id);
|
|
272
|
+
changed = true;
|
|
273
|
+
if (comment.user.type === "Bot")
|
|
274
|
+
continue;
|
|
275
|
+
await this.handleIncoming({
|
|
276
|
+
ref: { ...repo, number: issueNumberFromUrl(comment.issue_url) },
|
|
277
|
+
ts: String(comment.id),
|
|
278
|
+
user: comment.user.login,
|
|
279
|
+
text: comment.body,
|
|
280
|
+
createdAt: comment.created_at,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
pruneSeen(state.seenComments);
|
|
284
|
+
pruneSeen(state.seenIssues);
|
|
285
|
+
return changed;
|
|
286
|
+
}
|
|
287
|
+
// ==========================================================================
|
|
288
|
+
// Private - watermark persistence
|
|
289
|
+
// ==========================================================================
|
|
290
|
+
loadSyncState() {
|
|
291
|
+
try {
|
|
292
|
+
return readJsonSchemaFileIfExists(this.config.syncStatePath, SyncStateSchema, (detail) => `Malformed GitHub sync state at ${this.config.syncStatePath}: ${detail}`);
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
// A bad state file re-baselines (history won't trigger) rather than
|
|
296
|
+
// blocking startup; the next persist overwrites it.
|
|
297
|
+
log.logWarning("GitHub: ignoring unreadable sync state", err instanceof Error ? err.message : String(err));
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
persistSyncState() {
|
|
302
|
+
const state = { repos: {} };
|
|
303
|
+
for (const [repoKey, watermark] of this.repoState) {
|
|
304
|
+
state.repos[repoKey] = {
|
|
305
|
+
baseline: watermark.baseline,
|
|
306
|
+
cursor: watermark.cursor,
|
|
307
|
+
seenComments: [...watermark.seenComments],
|
|
308
|
+
seenIssues: [...watermark.seenIssues],
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
ensureDirExists(dirname(this.config.syncStatePath));
|
|
312
|
+
atomicWritePrivateFile(this.config.syncStatePath, JSON.stringify(state));
|
|
313
|
+
}
|
|
314
|
+
// ==========================================================================
|
|
315
|
+
// Private - triggering
|
|
316
|
+
// ==========================================================================
|
|
317
|
+
/** Fresh regex per call: the `g` flag makes `test`/`replace` stateful. */
|
|
318
|
+
mentionPattern() {
|
|
319
|
+
return this.appSlug ? new RegExp(`@${this.appSlug}(?![\\w-])`, "gi") : null;
|
|
320
|
+
}
|
|
321
|
+
isMentioned(text) {
|
|
322
|
+
const pattern = this.mentionPattern();
|
|
323
|
+
return pattern ? pattern.test(text) : false;
|
|
324
|
+
}
|
|
325
|
+
stripMention(text) {
|
|
326
|
+
const pattern = this.mentionPattern();
|
|
327
|
+
return (pattern ? text.replace(pattern, "") : text).trim();
|
|
328
|
+
}
|
|
329
|
+
isParticipating(conversationId) {
|
|
330
|
+
return existsSync(join(this.config.workingDir, conversationId, "log.jsonl"));
|
|
331
|
+
}
|
|
332
|
+
async handleIncoming(item) {
|
|
333
|
+
const conversationId = buildGithubConversationId(item.ref);
|
|
334
|
+
const mentioned = this.isMentioned(item.text);
|
|
335
|
+
const participating = this.isParticipating(conversationId);
|
|
336
|
+
// Narrow trigger (DESIGN.md): only threads the bot is mentioned in or
|
|
337
|
+
// already participates in. Everything else is ignored without logging so
|
|
338
|
+
// busy repos don't grow conversation dirs for untouched issues.
|
|
339
|
+
if (!mentioned && !participating)
|
|
340
|
+
return;
|
|
341
|
+
const cleanedText = this.stripMention(item.text);
|
|
342
|
+
const sessionKey = resolveChatSessionKey({
|
|
343
|
+
conversationId,
|
|
344
|
+
conversationKind: "shared",
|
|
345
|
+
messageId: item.ts,
|
|
346
|
+
persistentTopLevel: true,
|
|
347
|
+
});
|
|
348
|
+
if (/^stop$/i.test(cleanedText)) {
|
|
349
|
+
this.logToFile(conversationId, {
|
|
350
|
+
date: item.createdAt,
|
|
351
|
+
ts: item.ts,
|
|
352
|
+
user: item.user,
|
|
353
|
+
userName: item.user,
|
|
354
|
+
text: cleanedText,
|
|
355
|
+
attachments: [],
|
|
356
|
+
isMessagingBot: false,
|
|
357
|
+
});
|
|
358
|
+
const stopTarget = resolveStopTarget({ handler: this.handler, conversationId, sessionKey });
|
|
359
|
+
if (stopTarget) {
|
|
360
|
+
await this.handler.handleStop(stopTarget, conversationId, this);
|
|
361
|
+
}
|
|
362
|
+
else if (mentioned) {
|
|
363
|
+
await this.postMessage(conversationId, formatNothingRunning("github"));
|
|
364
|
+
}
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// First contact via a comment: log the issue title/body first so the
|
|
368
|
+
// session history starts with what the thread is about.
|
|
369
|
+
if (!participating && item.ts !== GITHUB_ISSUE_BODY_TS) {
|
|
370
|
+
await this.logIssueContext(item.ref, conversationId, item.createdAt);
|
|
371
|
+
}
|
|
372
|
+
const eventBase = {
|
|
373
|
+
type: item.ts === GITHUB_ISSUE_BODY_TS ? "issue" : "message",
|
|
374
|
+
conversationId,
|
|
375
|
+
conversationKind: "shared",
|
|
376
|
+
ts: item.ts,
|
|
377
|
+
sessionKey,
|
|
378
|
+
user: item.user,
|
|
379
|
+
userName: item.user,
|
|
380
|
+
text: cleanedText,
|
|
381
|
+
};
|
|
382
|
+
await processMessageIntake({
|
|
383
|
+
eventBase,
|
|
384
|
+
workingDir: this.config.workingDir,
|
|
385
|
+
isAutoReplyCandidate: false,
|
|
386
|
+
logEntryBase: {
|
|
387
|
+
date: item.createdAt,
|
|
388
|
+
ts: item.ts,
|
|
389
|
+
user: item.user,
|
|
390
|
+
userName: item.user,
|
|
391
|
+
text: cleanedText,
|
|
392
|
+
isMessagingBot: false,
|
|
393
|
+
},
|
|
394
|
+
log: (entry) => this.logToFile(conversationId, entry),
|
|
395
|
+
processAttachments: async () => [],
|
|
396
|
+
queueKey: conversationId,
|
|
397
|
+
enqueue: (queueKey, work) => this.getQueue(queueKey).enqueue(work),
|
|
398
|
+
handler: this.handler,
|
|
399
|
+
bot: this,
|
|
400
|
+
createContext: (event) => createGithubAdapters(event, this),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
async logIssueContext(ref, conversationId, triggerCreatedAt) {
|
|
404
|
+
let issue;
|
|
405
|
+
try {
|
|
406
|
+
issue = await githubRetry(() => this.client.getIssue(ref.owner, ref.repo, ref.number));
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
log.logWarning(`GitHub: could not fetch issue context for ${conversationId}`, err instanceof Error ? err.message : String(err));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
// Dated just before the triggering comment, NOT issue.created_at: history
|
|
413
|
+
// sync date-sorts entries and drops anything older than its recency
|
|
414
|
+
// window, so an old issue's real creation date would silently evict the
|
|
415
|
+
// one message that says what the thread is about.
|
|
416
|
+
const triggerMs = Date.parse(triggerCreatedAt);
|
|
417
|
+
const contextDate = new Date((Number.isFinite(triggerMs) ? triggerMs : Date.now()) - 1000);
|
|
418
|
+
this.logToFile(conversationId, {
|
|
419
|
+
date: contextDate.toISOString(),
|
|
420
|
+
ts: GITHUB_ISSUE_BODY_TS,
|
|
421
|
+
user: issue.user.login,
|
|
422
|
+
userName: issue.user.login,
|
|
423
|
+
text: `# ${issue.title}\n\n${issue.body ?? ""}`.trim(),
|
|
424
|
+
attachments: [],
|
|
425
|
+
isMessagingBot: false,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
//# sourceMappingURL=bot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bot.js","sourceRoot":"","sources":["../../../src/adapters/github/bot.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport { Type } from \"@sinclair/typebox\";\nimport type {\n ConversationEvent,\n MessagingBot,\n MessagingEventHandler,\n MessagingInfo,\n} from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport { ensureDirExists, readJsonSchemaFileIfExists } from \"../../utils/file-guards.js\";\nimport { atomicWritePrivateFile } from \"../../utils/fs-atomic.js\";\nimport { resolveChatSessionKey } from \"../../sessions/policy.js\";\nimport { formatNothingRunning } from \"../../platform-messages.js\";\nimport {\n appendBotResponseLog,\n appendChannelLog,\n MessagingEventQueue,\n resolveStopTarget,\n splitText,\n withRetry,\n} from \"../shared.js\";\nimport { processMessageIntake } from \"../intake.js\";\nimport { GithubClient, githubIsRateLimited } from \"./client.js\";\nimport { createGithubAdapters } from \"./context.js\";\nimport {\n buildGithubConversationId,\n GITHUB_ISSUE_BODY_TS,\n parseGithubConversationId,\n} from \"./ids.js\";\nimport type {\n GithubBotConfig,\n GithubConversationRef,\n GithubEvent,\n GithubIssue,\n GithubReactionContent,\n GithubRepoRef,\n GithubSyncState,\n} from \"./types.js\";\n\nconst SyncStateSchema = Type.Object({\n repos: Type.Record(\n Type.String(),\n Type.Object({\n baseline: Type.String(),\n cursor: Type.String(),\n seenComments: Type.Array(Type.Number()),\n seenIssues: Type.Array(Type.Number()),\n }),\n ),\n});\n\n/**\n * Re-fetch this far behind the cursor each poll. Dedup is by id (the\n * watermark), so the overlap only costs bandwidth and closes the boundary\n * races of a bare `since` timestamp (DESIGN.md § Event source).\n */\nconst POLL_OVERLAP_MS = 5 * 60 * 1000;\n\nconst MAX_SEEN_IDS = 5000;\n\n/** In-memory form of one repo's persisted watermark. */\ninterface RepoWatermark {\n baseline: string;\n cursor: string;\n seenComments: Set<number>;\n seenIssues: Set<number>;\n}\n\nconst githubRetry = <T>(fn: () => Promise<T>): Promise<T> =>\n withRetry(fn, { isRateLimited: githubIsRateLimited });\n\n/** GitHub comment bodies cap at 65536 chars; leave headroom for markup. */\nexport const GITHUB_MAX_COMMENT_LENGTH = 60000;\n\nexport const formatGithubContinuation = (partNum: number): string => `*(continued ${partNum})*`;\n\n/** Slack-style emoji short names → GitHub reaction content values. */\nconst GITHUB_REACTIONS: Record<string, GithubReactionContent> = {\n \"+1\": \"+1\",\n thumbsup: \"+1\",\n \"-1\": \"-1\",\n thumbsdown: \"-1\",\n laugh: \"laugh\",\n smile: \"laugh\",\n confused: \"confused\",\n heart: \"heart\",\n hooray: \"hooray\",\n tada: \"hooray\",\n rocket: \"rocket\",\n eyes: \"eyes\",\n};\n\ninterface IncomingItem {\n ref: GithubConversationRef;\n /** Comment id, or GITHUB_ISSUE_BODY_TS for the issue/PR body itself. */\n ts: string;\n user: string;\n text: string;\n createdAt: string;\n}\n\n// Repo refs are lowercased at every entry point (env config here, the\n// installation API in start()) so conversation ids and sync-state keys never\n// depend on how a repo's name was spelled — GitHub is case-insensitive.\nfunction parseRepoList(repos: string[]): GithubRepoRef[] {\n return repos.map((entry) => {\n const [owner, repo, ...rest] = entry.split(\"/\");\n if (!owner || !repo || rest.length > 0) {\n throw new Error(`Invalid GITHUB_REPOS entry (expected owner/repo): ${entry}`);\n }\n return { owner: owner.toLowerCase(), repo: repo.toLowerCase() };\n });\n}\n\n/** Sets iterate in insertion order; drop the oldest ids once over the cap. */\nfunction pruneSeen(seen: Set<number>): void {\n if (seen.size <= MAX_SEEN_IDS) return;\n let toDrop = seen.size - MAX_SEEN_IDS / 2;\n for (const id of seen) {\n if (toDrop-- <= 0) break;\n seen.delete(id);\n }\n}\n\n/** Issue number from a comment's `issue_url` (…/repos/o/r/issues/42). */\nfunction issueNumberFromUrl(issueUrl: string): number {\n const match = /\\/issues\\/(\\d+)$/.exec(issueUrl);\n if (!match) {\n throw new Error(`Cannot parse issue number from ${issueUrl}`);\n }\n return Number(match[1]);\n}\n\nexport class GithubMessagingBot implements MessagingBot {\n private readonly client: GithubClient;\n private readonly handler: MessagingEventHandler;\n private readonly config: GithubBotConfig;\n private appSlug: string | null = null;\n private watchedRepos: GithubRepoRef[] = [];\n private queues = new Map<string, MessagingEventQueue>();\n private repoState = new Map<string, RepoWatermark>();\n private pollInFlight = false;\n\n constructor(handler: MessagingEventHandler, config: GithubBotConfig, client?: GithubClient) {\n this.handler = handler;\n this.config = config;\n this.client =\n client ??\n new GithubClient({\n appId: config.appId,\n privateKey: config.privateKey,\n installationId: config.installationId,\n });\n }\n\n // ==========================================================================\n // Public API (implements MessagingBot)\n // ==========================================================================\n\n async start(): Promise<void> {\n this.appSlug = await this.client.getAppSlug();\n this.watchedRepos =\n this.config.repos.length > 0\n ? parseRepoList(this.config.repos)\n : (await this.client.listInstallationRepositories()).map((repository) => ({\n owner: repository.owner.login.toLowerCase(),\n repo: repository.name.toLowerCase(),\n }));\n if (this.watchedRepos.length === 0) {\n log.logWarning(\"GitHub: installation has no repositories; nothing to poll\");\n }\n\n // Restore persisted watermarks; a repo watched for the first time gets a\n // baseline of \"now\" so history never triggers (DESIGN.md § Event source).\n const persisted = this.loadSyncState();\n const now = new Date().toISOString();\n for (const repo of this.watchedRepos) {\n const repoKey = `${repo.owner}/${repo.repo}`;\n const saved = persisted?.repos[repoKey];\n this.repoState.set(\n repoKey,\n saved\n ? {\n baseline: saved.baseline,\n cursor: saved.cursor,\n seenComments: new Set(saved.seenComments),\n seenIssues: new Set(saved.seenIssues),\n }\n : { baseline: now, cursor: now, seenComments: new Set(), seenIssues: new Set() },\n );\n }\n this.persistSyncState();\n\n setInterval(() => {\n void this.poll();\n }, this.config.pollIntervalMs).unref();\n\n log.logConnected(\"GitHub\");\n log.logInfo(\n `GitHub bot started as @${this.appSlug}[bot], polling ${this.watchedRepos.length} repo(s) every ${Math.round(this.config.pollIntervalMs / 1000)}s`,\n );\n }\n\n async postMessage(channel: string, text: string): Promise<string> {\n const ref = parseGithubConversationId(channel);\n const parts = splitText(text, GITHUB_MAX_COMMENT_LENGTH, formatGithubContinuation);\n const firstId = await this.postComment(ref, parts[0]);\n for (const part of parts.slice(1)) {\n await this.postComment(ref, part);\n }\n return String(firstId);\n }\n\n async updateMessage(channel: string, ts: string, text: string): Promise<void> {\n const ref = parseGithubConversationId(channel);\n await githubRetry(() => this.client.updateIssueComment(ref.owner, ref.repo, Number(ts), text));\n }\n\n async addReaction(channel: string, messageTs: string, emoji: string): Promise<void> {\n const ref = parseGithubConversationId(channel);\n const content = GITHUB_REACTIONS[emoji.replace(/^:|:$/g, \"\")];\n if (!content) {\n throw new Error(\n `GitHub does not support reaction '${emoji}' (supported: ${Object.keys(GITHUB_REACTIONS).join(\", \")})`,\n );\n }\n await githubRetry(() =>\n messageTs === GITHUB_ISSUE_BODY_TS\n ? this.client.createIssueReaction(ref.owner, ref.repo, ref.number, content)\n : this.client.createCommentReaction(ref.owner, ref.repo, Number(messageTs), content),\n );\n }\n\n enqueueEvent(event: ConversationEvent): boolean {\n const conversationId = event.conversationId;\n const queue = this.getQueue(conversationId);\n if (queue.size() >= 5) {\n log.logWarning(\n `Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`,\n );\n return false;\n }\n log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);\n queue.enqueue(() => {\n const context = createGithubAdapters(event as GithubEvent, this);\n return this.handler.handleEvent(event, this, context);\n });\n return true;\n }\n\n getMessagingInfo(): MessagingInfo {\n return {\n name: \"github\",\n formattingGuide:\n \"## GitHub Formatting (GitHub Flavored Markdown)\\n\" +\n \"Standard Markdown plus tables, task lists, and ```suggestion blocks.\\n\" +\n \"Reference issues/PRs as #123 and users as @login.\",\n channels: [],\n users: [],\n };\n }\n\n // ==========================================================================\n // Internal helpers (used by context.ts)\n // ==========================================================================\n\n async postComment(ref: GithubConversationRef, text: string): Promise<number> {\n const comment = await githubRetry(() =>\n this.client.createIssueComment(ref.owner, ref.repo, ref.number, text),\n );\n return comment.id;\n }\n\n async deleteComment(ref: GithubConversationRef, commentId: number): Promise<void> {\n await githubRetry(() => this.client.deleteIssueComment(ref.owner, ref.repo, commentId));\n }\n\n logToFile(conversationId: string, entry: object): void {\n appendChannelLog(this.config.workingDir, conversationId, entry);\n }\n\n logBotResponse(conversationId: string, text: string, ts: string): void {\n appendBotResponseLog(this.config.workingDir, conversationId, text, ts);\n }\n\n // ==========================================================================\n // Private - polling\n // ==========================================================================\n\n private getQueue(conversationId: string): MessagingEventQueue {\n let queue = this.queues.get(conversationId);\n if (!queue) {\n queue = new MessagingEventQueue(\"GitHub\");\n this.queues.set(conversationId, queue);\n }\n return queue;\n }\n\n /** One poll tick over every watched repo. Public for tests. */\n async poll(): Promise<void> {\n if (this.pollInFlight) return;\n this.pollInFlight = true;\n try {\n let changed = false;\n for (const repo of this.watchedRepos) {\n try {\n changed = (await this.pollRepo(repo)) || changed;\n } catch (err) {\n log.logWarning(\n `GitHub poll failed for ${repo.owner}/${repo.repo}`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n if (changed) {\n this.persistSyncState();\n }\n } finally {\n this.pollInFlight = false;\n }\n }\n\n private async pollRepo(repo: GithubRepoRef): Promise<boolean> {\n const repoKey = `${repo.owner}/${repo.repo}`;\n const state = this.repoState.get(repoKey);\n if (!state) return false;\n const since = new Date(Date.parse(state.cursor) - POLL_OVERLAP_MS).toISOString();\n let changed = false;\n\n // Comment activity also bumps the parent issue's updated_at, so the issues\n // list mostly echoes the comments list; only issues *created* since the\n // baseline are new conversations (their body is the first message).\n const issues = (await this.client.listIssuesSince(repo.owner, repo.repo, since)) ?? [];\n for (const issue of issues) {\n if (issue.updated_at > state.cursor) {\n state.cursor = issue.updated_at;\n changed = true;\n }\n // Edits re-surface old items with a fresh updated_at; the id watermark\n // plus the creation baseline decide what actually triggers.\n if (issue.created_at < state.baseline || state.seenIssues.has(issue.id)) continue;\n state.seenIssues.add(issue.id);\n changed = true;\n if (issue.user.type === \"Bot\") continue;\n await this.handleIncoming({\n ref: { ...repo, number: issue.number },\n ts: GITHUB_ISSUE_BODY_TS,\n user: issue.user.login,\n text: `# ${issue.title}\\n\\n${issue.body ?? \"\"}`.trim(),\n createdAt: issue.created_at,\n });\n }\n\n const comments = (await this.client.listIssueCommentsSince(repo.owner, repo.repo, since)) ?? [];\n for (const comment of comments) {\n if (comment.updated_at > state.cursor) {\n state.cursor = comment.updated_at;\n changed = true;\n }\n if (comment.created_at < state.baseline || state.seenComments.has(comment.id)) continue;\n state.seenComments.add(comment.id);\n changed = true;\n if (comment.user.type === \"Bot\") continue;\n await this.handleIncoming({\n ref: { ...repo, number: issueNumberFromUrl(comment.issue_url) },\n ts: String(comment.id),\n user: comment.user.login,\n text: comment.body,\n createdAt: comment.created_at,\n });\n }\n\n pruneSeen(state.seenComments);\n pruneSeen(state.seenIssues);\n return changed;\n }\n\n // ==========================================================================\n // Private - watermark persistence\n // ==========================================================================\n\n private loadSyncState(): GithubSyncState | undefined {\n try {\n return readJsonSchemaFileIfExists(\n this.config.syncStatePath,\n SyncStateSchema,\n (detail) => `Malformed GitHub sync state at ${this.config.syncStatePath}: ${detail}`,\n );\n } catch (err) {\n // A bad state file re-baselines (history won't trigger) rather than\n // blocking startup; the next persist overwrites it.\n log.logWarning(\n \"GitHub: ignoring unreadable sync state\",\n err instanceof Error ? err.message : String(err),\n );\n return undefined;\n }\n }\n\n private persistSyncState(): void {\n const state: GithubSyncState = { repos: {} };\n for (const [repoKey, watermark] of this.repoState) {\n state.repos[repoKey] = {\n baseline: watermark.baseline,\n cursor: watermark.cursor,\n seenComments: [...watermark.seenComments],\n seenIssues: [...watermark.seenIssues],\n };\n }\n ensureDirExists(dirname(this.config.syncStatePath));\n atomicWritePrivateFile(this.config.syncStatePath, JSON.stringify(state));\n }\n\n // ==========================================================================\n // Private - triggering\n // ==========================================================================\n\n /** Fresh regex per call: the `g` flag makes `test`/`replace` stateful. */\n private mentionPattern(): RegExp | null {\n return this.appSlug ? new RegExp(`@${this.appSlug}(?![\\\\w-])`, \"gi\") : null;\n }\n\n private isMentioned(text: string): boolean {\n const pattern = this.mentionPattern();\n return pattern ? pattern.test(text) : false;\n }\n\n private stripMention(text: string): string {\n const pattern = this.mentionPattern();\n return (pattern ? text.replace(pattern, \"\") : text).trim();\n }\n\n private isParticipating(conversationId: string): boolean {\n return existsSync(join(this.config.workingDir, conversationId, \"log.jsonl\"));\n }\n\n private async handleIncoming(item: IncomingItem): Promise<void> {\n const conversationId = buildGithubConversationId(item.ref);\n const mentioned = this.isMentioned(item.text);\n const participating = this.isParticipating(conversationId);\n // Narrow trigger (DESIGN.md): only threads the bot is mentioned in or\n // already participates in. Everything else is ignored without logging so\n // busy repos don't grow conversation dirs for untouched issues.\n if (!mentioned && !participating) return;\n\n const cleanedText = this.stripMention(item.text);\n const sessionKey = resolveChatSessionKey({\n conversationId,\n conversationKind: \"shared\",\n messageId: item.ts,\n persistentTopLevel: true,\n });\n\n if (/^stop$/i.test(cleanedText)) {\n this.logToFile(conversationId, {\n date: item.createdAt,\n ts: item.ts,\n user: item.user,\n userName: item.user,\n text: cleanedText,\n attachments: [],\n isMessagingBot: false,\n });\n const stopTarget = resolveStopTarget({ handler: this.handler, conversationId, sessionKey });\n if (stopTarget) {\n await this.handler.handleStop(stopTarget, conversationId, this);\n } else if (mentioned) {\n await this.postMessage(conversationId, formatNothingRunning(\"github\"));\n }\n return;\n }\n\n // First contact via a comment: log the issue title/body first so the\n // session history starts with what the thread is about.\n if (!participating && item.ts !== GITHUB_ISSUE_BODY_TS) {\n await this.logIssueContext(item.ref, conversationId, item.createdAt);\n }\n\n const eventBase: GithubEvent = {\n type: item.ts === GITHUB_ISSUE_BODY_TS ? \"issue\" : \"message\",\n conversationId,\n conversationKind: \"shared\",\n ts: item.ts,\n sessionKey,\n user: item.user,\n userName: item.user,\n text: cleanedText,\n };\n\n await processMessageIntake({\n eventBase,\n workingDir: this.config.workingDir,\n isAutoReplyCandidate: false,\n logEntryBase: {\n date: item.createdAt,\n ts: item.ts,\n user: item.user,\n userName: item.user,\n text: cleanedText,\n isMessagingBot: false,\n },\n log: (entry) => this.logToFile(conversationId, entry),\n processAttachments: async () => [],\n queueKey: conversationId,\n enqueue: (queueKey, work) => this.getQueue(queueKey).enqueue(work),\n handler: this.handler,\n bot: this,\n createContext: (event) => createGithubAdapters(event, this),\n });\n }\n\n private async logIssueContext(\n ref: GithubConversationRef,\n conversationId: string,\n triggerCreatedAt: string,\n ): Promise<void> {\n let issue: GithubIssue;\n try {\n issue = await githubRetry(() => this.client.getIssue(ref.owner, ref.repo, ref.number));\n } catch (err) {\n log.logWarning(\n `GitHub: could not fetch issue context for ${conversationId}`,\n err instanceof Error ? err.message : String(err),\n );\n return;\n }\n // Dated just before the triggering comment, NOT issue.created_at: history\n // sync date-sorts entries and drops anything older than its recency\n // window, so an old issue's real creation date would silently evict the\n // one message that says what the thread is about.\n const triggerMs = Date.parse(triggerCreatedAt);\n const contextDate = new Date((Number.isFinite(triggerMs) ? triggerMs : Date.now()) - 1000);\n this.logToFile(conversationId, {\n date: contextDate.toISOString(),\n ts: GITHUB_ISSUE_BODY_TS,\n user: issue.user.login,\n userName: issue.user.login,\n text: `# ${issue.title}\\n\\n${issue.body ?? \"\"}`.trim(),\n attachments: [],\n isMessagingBot: false,\n });\n }\n}\n"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { GithubClientOptions, GithubIssue, GithubIssueComment, GithubReactionContent, GithubRepository } from "./types.js";
|
|
2
|
+
export declare class GithubApiError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
constructor(status: number, method: string, path: string, detail: string);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* GitHub rate-limit responses are 429, or 403 with a rate-limit message
|
|
8
|
+
* (secondary limits). Used as the `withRetry` predicate.
|
|
9
|
+
*/
|
|
10
|
+
export declare function githubIsRateLimited(err: Error): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Minimal GitHub REST client authenticated as a GitHub App installation.
|
|
13
|
+
* Mints short-lived installation tokens from an RS256 app JWT and supports
|
|
14
|
+
* ETag conditional polling (304 responses don't count against the rate limit).
|
|
15
|
+
*/
|
|
16
|
+
export declare class GithubClient {
|
|
17
|
+
private readonly appId;
|
|
18
|
+
private readonly privateKey;
|
|
19
|
+
private readonly installationId;
|
|
20
|
+
private readonly baseUrl;
|
|
21
|
+
private readonly fetchImpl;
|
|
22
|
+
private installationToken;
|
|
23
|
+
private etags;
|
|
24
|
+
constructor(options: GithubClientOptions);
|
|
25
|
+
/** Short-lived JWT identifying the App itself (not an installation). */
|
|
26
|
+
private appJwt;
|
|
27
|
+
private getInstallationToken;
|
|
28
|
+
private rawRequest;
|
|
29
|
+
/**
|
|
30
|
+
* Installation-authenticated request. `conditional` GETs return null on 304
|
|
31
|
+
* (nothing changed since the last call to the same path).
|
|
32
|
+
*/
|
|
33
|
+
private request;
|
|
34
|
+
/** The App's mention slug; its comments author as `<slug>[bot]`. */
|
|
35
|
+
getAppSlug(): Promise<string>;
|
|
36
|
+
listInstallationRepositories(): Promise<GithubRepository[]>;
|
|
37
|
+
/**
|
|
38
|
+
* All issue/PR conversation comments in a repo updated at or after `since`,
|
|
39
|
+
* oldest first. Returns null when nothing changed (ETag 304). Capped at one
|
|
40
|
+
* page; the poll cursor advances to the newest item seen, so a burst larger
|
|
41
|
+
* than 100 comments drains across consecutive polls.
|
|
42
|
+
*/
|
|
43
|
+
listIssueCommentsSince(owner: string, repo: string, since: string): Promise<GithubIssueComment[] | null>;
|
|
44
|
+
/** Issues and PRs updated at or after `since` (GitHub lists PRs as issues). */
|
|
45
|
+
listIssuesSince(owner: string, repo: string, since: string): Promise<GithubIssue[] | null>;
|
|
46
|
+
getIssue(owner: string, repo: string, number: number): Promise<GithubIssue>;
|
|
47
|
+
createIssueComment(owner: string, repo: string, number: number, body: string): Promise<GithubIssueComment>;
|
|
48
|
+
updateIssueComment(owner: string, repo: string, commentId: number, body: string): Promise<void>;
|
|
49
|
+
deleteIssueComment(owner: string, repo: string, commentId: number): Promise<void>;
|
|
50
|
+
createCommentReaction(owner: string, repo: string, commentId: number, content: GithubReactionContent): Promise<void>;
|
|
51
|
+
createIssueReaction(owner: string, repo: string, number: number, content: GithubReactionContent): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/adapters/github/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,mBAAmB,EACnB,WAAW,EACX,kBAAkB,EAClB,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB,qBAAa,cAAe,SAAQ,KAAK;aAErB,MAAM,EAAE,MAAM;IADhC,YACkB,MAAM,EAAE,MAAM,EAC9B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EAIf;CACF;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,KAAK,GAAG,OAAO,CAGvD;AASD;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,iBAAiB,CAAqD;IAC9E,OAAO,CAAC,KAAK,CAA6B;IAE1C,YAAY,OAAO,EAAE,mBAAmB,EAMvC;IAED,wEAAwE;IACxE,OAAO,CAAC,MAAM;YAWA,oBAAoB;YAcpB,UAAU;IAqCxB;;;OAGG;YACW,OAAO;IASrB,oEAAoE;IAC9D,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAKlC;IAEK,4BAA4B,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAMhE;IAED;;;;;OAKG;IACG,sBAAsB,CAC1B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,kBAAkB,EAAE,GAAG,IAAI,CAAC,CAMtC;IAED,+EAA+E;IACzE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,CAM/F;IAEK,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAMhF;IAEK,kBAAkB,CACtB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,kBAAkB,CAAC,CAO7B;IAEK,kBAAkB,CACtB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAIf;IAEK,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEtF;IAEK,qBAAqB,CACzB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAIf;IAEK,mBAAmB,CACvB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAIf;CACF"}
|