@nairon-ai/aegis 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/.agents/skills/bug-fix/SKILL.md +91 -0
  2. package/.flue/agents/bug-fix.ts +107 -0
  3. package/.flue/app.ts +16 -0
  4. package/Dockerfile +8 -0
  5. package/LICENSE +21 -0
  6. package/README.md +251 -0
  7. package/dist-node/agent/bug-fix-skill.d.ts +2 -0
  8. package/dist-node/agent/bug-fix-skill.d.ts.map +1 -0
  9. package/dist-node/agent/bug-fix-skill.js +64 -0
  10. package/dist-node/agent/bug-fix-skill.js.map +1 -0
  11. package/dist-node/agent/client.d.ts +14 -0
  12. package/dist-node/agent/client.d.ts.map +1 -0
  13. package/dist-node/agent/client.js +110 -0
  14. package/dist-node/agent/client.js.map +1 -0
  15. package/dist-node/cli/commands/deploy.d.ts +3 -0
  16. package/dist-node/cli/commands/deploy.d.ts.map +1 -0
  17. package/dist-node/cli/commands/deploy.js +94 -0
  18. package/dist-node/cli/commands/deploy.js.map +1 -0
  19. package/dist-node/cli/commands/init.d.ts +3 -0
  20. package/dist-node/cli/commands/init.d.ts.map +1 -0
  21. package/dist-node/cli/commands/init.js +115 -0
  22. package/dist-node/cli/commands/init.js.map +1 -0
  23. package/dist-node/cli/commands/pickup.d.ts +11 -0
  24. package/dist-node/cli/commands/pickup.d.ts.map +1 -0
  25. package/dist-node/cli/commands/pickup.js +43 -0
  26. package/dist-node/cli/commands/pickup.js.map +1 -0
  27. package/dist-node/cli/commands/setup.d.ts +3 -0
  28. package/dist-node/cli/commands/setup.d.ts.map +1 -0
  29. package/dist-node/cli/commands/setup.js +163 -0
  30. package/dist-node/cli/commands/setup.js.map +1 -0
  31. package/dist-node/cli/commands/status.d.ts +3 -0
  32. package/dist-node/cli/commands/status.d.ts.map +1 -0
  33. package/dist-node/cli/commands/status.js +26 -0
  34. package/dist-node/cli/commands/status.js.map +1 -0
  35. package/dist-node/cli/index.d.ts +3 -0
  36. package/dist-node/cli/index.d.ts.map +1 -0
  37. package/dist-node/cli/index.js +36 -0
  38. package/dist-node/cli/index.js.map +1 -0
  39. package/dist-node/cli/paths.d.ts +7 -0
  40. package/dist-node/cli/paths.d.ts.map +1 -0
  41. package/dist-node/cli/paths.js +29 -0
  42. package/dist-node/cli/paths.js.map +1 -0
  43. package/dist-node/cli/state.d.ts +16 -0
  44. package/dist-node/cli/state.d.ts.map +1 -0
  45. package/dist-node/cli/state.js +214 -0
  46. package/dist-node/cli/state.js.map +1 -0
  47. package/dist-node/core/pickup.d.ts +14 -0
  48. package/dist-node/core/pickup.d.ts.map +1 -0
  49. package/dist-node/core/pickup.js +42 -0
  50. package/dist-node/core/pickup.js.map +1 -0
  51. package/dist-node/github/index.d.ts +3 -0
  52. package/dist-node/github/index.d.ts.map +1 -0
  53. package/dist-node/github/index.js +2 -0
  54. package/dist-node/github/index.js.map +1 -0
  55. package/dist-node/github/manifest.d.ts +34 -0
  56. package/dist-node/github/manifest.d.ts.map +1 -0
  57. package/dist-node/github/manifest.js +71 -0
  58. package/dist-node/github/manifest.js.map +1 -0
  59. package/dist-node/integrations/github.d.ts +29 -0
  60. package/dist-node/integrations/github.d.ts.map +1 -0
  61. package/dist-node/integrations/github.js +199 -0
  62. package/dist-node/integrations/github.js.map +1 -0
  63. package/dist-node/integrations/linear.d.ts +15 -0
  64. package/dist-node/integrations/linear.d.ts.map +1 -0
  65. package/dist-node/integrations/linear.js +146 -0
  66. package/dist-node/integrations/linear.js.map +1 -0
  67. package/dist-node/integrations/telegram.d.ts +24 -0
  68. package/dist-node/integrations/telegram.d.ts.map +1 -0
  69. package/dist-node/integrations/telegram.js +39 -0
  70. package/dist-node/integrations/telegram.js.map +1 -0
  71. package/dist-node/integrations/webhooks.d.ts +3 -0
  72. package/dist-node/integrations/webhooks.d.ts.map +1 -0
  73. package/dist-node/integrations/webhooks.js +37 -0
  74. package/dist-node/integrations/webhooks.js.map +1 -0
  75. package/dist-node/sandbox/github-token.d.ts +7 -0
  76. package/dist-node/sandbox/github-token.d.ts.map +1 -0
  77. package/dist-node/sandbox/github-token.js +66 -0
  78. package/dist-node/sandbox/github-token.js.map +1 -0
  79. package/dist-node/sandbox/index.d.ts +2 -0
  80. package/dist-node/sandbox/index.d.ts.map +1 -0
  81. package/dist-node/sandbox/index.js +2 -0
  82. package/dist-node/sandbox/index.js.map +1 -0
  83. package/dist-node/server/app.d.ts +9 -0
  84. package/dist-node/server/app.d.ts.map +1 -0
  85. package/dist-node/server/app.js +216 -0
  86. package/dist-node/server/app.js.map +1 -0
  87. package/dist-node/shared/config.d.ts +5 -0
  88. package/dist-node/shared/config.d.ts.map +1 -0
  89. package/dist-node/shared/config.js +135 -0
  90. package/dist-node/shared/config.js.map +1 -0
  91. package/dist-node/shared/constants.d.ts +16 -0
  92. package/dist-node/shared/constants.d.ts.map +1 -0
  93. package/dist-node/shared/constants.js +29 -0
  94. package/dist-node/shared/constants.js.map +1 -0
  95. package/dist-node/shared/format.d.ts +12 -0
  96. package/dist-node/shared/format.d.ts.map +1 -0
  97. package/dist-node/shared/format.js +71 -0
  98. package/dist-node/shared/format.js.map +1 -0
  99. package/dist-node/shared/index.d.ts +7 -0
  100. package/dist-node/shared/index.d.ts.map +1 -0
  101. package/dist-node/shared/index.js +7 -0
  102. package/dist-node/shared/index.js.map +1 -0
  103. package/dist-node/shared/readiness.d.ts +3 -0
  104. package/dist-node/shared/readiness.d.ts.map +1 -0
  105. package/dist-node/shared/readiness.js +91 -0
  106. package/dist-node/shared/readiness.js.map +1 -0
  107. package/dist-node/shared/run-state.d.ts +5 -0
  108. package/dist-node/shared/run-state.d.ts.map +1 -0
  109. package/dist-node/shared/run-state.js +26 -0
  110. package/dist-node/shared/run-state.js.map +1 -0
  111. package/dist-node/shared/types.d.ts +230 -0
  112. package/dist-node/shared/types.d.ts.map +1 -0
  113. package/dist-node/shared/types.js +5 -0
  114. package/dist-node/shared/types.js.map +1 -0
  115. package/dist-node/sources/github.d.ts +15 -0
  116. package/dist-node/sources/github.d.ts.map +1 -0
  117. package/dist-node/sources/github.js +44 -0
  118. package/dist-node/sources/github.js.map +1 -0
  119. package/dist-node/sources/index.d.ts +6 -0
  120. package/dist-node/sources/index.d.ts.map +1 -0
  121. package/dist-node/sources/index.js +16 -0
  122. package/dist-node/sources/index.js.map +1 -0
  123. package/dist-node/sources/linear.d.ts +15 -0
  124. package/dist-node/sources/linear.d.ts.map +1 -0
  125. package/dist-node/sources/linear.js +32 -0
  126. package/dist-node/sources/linear.js.map +1 -0
  127. package/dist-node/sources/types.d.ts +15 -0
  128. package/dist-node/sources/types.d.ts.map +1 -0
  129. package/dist-node/sources/types.js +2 -0
  130. package/dist-node/sources/types.js.map +1 -0
  131. package/docs/RELEASING.md +52 -0
  132. package/docs/SETUP.md +439 -0
  133. package/package.json +64 -0
  134. package/src/agent/bug-fix-skill.ts +63 -0
  135. package/src/agent/client.ts +156 -0
  136. package/src/cli/commands/deploy.ts +106 -0
  137. package/src/cli/commands/init.ts +119 -0
  138. package/src/cli/commands/pickup.ts +44 -0
  139. package/src/cli/commands/setup.ts +217 -0
  140. package/src/cli/commands/status.ts +24 -0
  141. package/src/cli/index.ts +38 -0
  142. package/src/cli/paths.ts +29 -0
  143. package/src/cli/state.ts +228 -0
  144. package/src/core/pickup.ts +66 -0
  145. package/src/github/index.ts +2 -0
  146. package/src/github/manifest.ts +97 -0
  147. package/src/integrations/github.ts +241 -0
  148. package/src/integrations/linear.ts +195 -0
  149. package/src/integrations/telegram.ts +48 -0
  150. package/src/integrations/webhooks.ts +53 -0
  151. package/src/sandbox/github-token.ts +92 -0
  152. package/src/sandbox/index.ts +1 -0
  153. package/src/server/app.ts +292 -0
  154. package/src/shared/config.ts +154 -0
  155. package/src/shared/constants.ts +30 -0
  156. package/src/shared/format.ts +84 -0
  157. package/src/shared/index.ts +6 -0
  158. package/src/shared/readiness.ts +116 -0
  159. package/src/shared/run-state.ts +32 -0
  160. package/src/shared/types.ts +257 -0
  161. package/src/sources/github.ts +57 -0
  162. package/src/sources/index.ts +20 -0
  163. package/src/sources/linear.ts +44 -0
  164. package/src/sources/types.ts +16 -0
  165. package/tsconfig.json +25 -0
  166. package/tsconfig.node.json +16 -0
  167. package/wrangler.jsonc +43 -0
@@ -0,0 +1,195 @@
1
+ import type { AegisConfig, WorkItem } from "../shared/types.js";
2
+
3
+ type LinearIssueNode = {
4
+ id: string;
5
+ identifier: string;
6
+ title: string;
7
+ description?: string | null;
8
+ url: string;
9
+ createdAt: string;
10
+ updatedAt: string;
11
+ state?: { id: string; name: string };
12
+ team?: { id: string; key: string };
13
+ labels?: { nodes: Array<{ name: string }> };
14
+ };
15
+
16
+ type LinearStateNode = {
17
+ id: string;
18
+ name: string;
19
+ };
20
+
21
+ export class LinearClient {
22
+ private readonly config: AegisConfig;
23
+
24
+ constructor(config: AegisConfig) {
25
+ this.config = config;
26
+ }
27
+
28
+ isConfigured(): boolean {
29
+ return Boolean(this.config.linear?.apiKey && this.config.linear.teamId);
30
+ }
31
+
32
+ async listReadyIssues(): Promise<WorkItem[]> {
33
+ if (!this.isConfigured()) return [];
34
+ const linear = this.requireConfig();
35
+ const result = await this.graphql<{
36
+ issues: { nodes: LinearIssueNode[] };
37
+ }>(
38
+ `query ReadyIssues($filter: IssueFilter, $first: Int!) {
39
+ issues(filter: $filter, first: $first, orderBy: updatedAt) {
40
+ nodes {
41
+ id
42
+ identifier
43
+ title
44
+ description
45
+ url
46
+ createdAt
47
+ updatedAt
48
+ state { id name }
49
+ team { id key }
50
+ labels { nodes { name } }
51
+ }
52
+ }
53
+ }`,
54
+ {
55
+ first: 50,
56
+ filter: {
57
+ team: { id: { eq: linear.teamId } },
58
+ ...(linear.projectId ? { project: { id: { eq: linear.projectId } } } : {}),
59
+ state: { name: { eq: linear.readyStatusName } },
60
+ labels: { some: { name: { eq: linear.bugLabel } } },
61
+ },
62
+ },
63
+ );
64
+
65
+ return result.issues.nodes
66
+ .filter((issue) => {
67
+ const labels = issue.labels?.nodes.map((label) => label.name.toLowerCase()) ?? [];
68
+ return ![
69
+ this.config.inProgressLabel,
70
+ this.config.needsInfoLabel,
71
+ this.config.blockedLabel,
72
+ this.config.prOpenedLabel,
73
+ ].some((label) => labels.includes(label.toLowerCase()));
74
+ })
75
+ .map((issue) => ({
76
+ source: "linear" as const,
77
+ id: issue.id,
78
+ identifier: issue.identifier,
79
+ title: issue.title,
80
+ body: issue.description ?? "",
81
+ url: issue.url,
82
+ repo: this.config.monitoredRepo,
83
+ labels: issue.labels?.nodes.map((label) => label.name) ?? [],
84
+ statusName: issue.state?.name,
85
+ createdAt: issue.createdAt,
86
+ updatedAt: issue.updatedAt,
87
+ }));
88
+ }
89
+
90
+ async getIssue(issueIdOrIdentifier: string): Promise<WorkItem> {
91
+ const result = await this.graphql<{ issue: LinearIssueNode }>(
92
+ `query Issue($id: String!) {
93
+ issue(id: $id) {
94
+ id
95
+ identifier
96
+ title
97
+ description
98
+ url
99
+ createdAt
100
+ updatedAt
101
+ state { id name }
102
+ team { id key }
103
+ labels { nodes { name } }
104
+ }
105
+ }`,
106
+ { id: issueIdOrIdentifier },
107
+ );
108
+ const issue = result.issue;
109
+ return {
110
+ source: "linear",
111
+ id: issue.id,
112
+ identifier: issue.identifier,
113
+ title: issue.title,
114
+ body: issue.description ?? "",
115
+ url: issue.url,
116
+ repo: this.config.monitoredRepo,
117
+ labels: issue.labels?.nodes.map((label) => label.name) ?? [],
118
+ statusName: issue.state?.name,
119
+ createdAt: issue.createdAt,
120
+ updatedAt: issue.updatedAt,
121
+ };
122
+ }
123
+
124
+ async comment(issueId: string, body: string): Promise<void> {
125
+ await this.graphql(
126
+ `mutation CreateComment($issueId: String!, $body: String!) {
127
+ commentCreate(input: { issueId: $issueId, body: $body }) {
128
+ success
129
+ }
130
+ }`,
131
+ { issueId, body },
132
+ );
133
+ }
134
+
135
+ async moveToStatus(issueId: string, statusName: string | undefined): Promise<void> {
136
+ if (!statusName) return;
137
+ const stateId = await this.findStateId(statusName);
138
+ if (!stateId) return;
139
+ await this.graphql(
140
+ `mutation UpdateIssue($id: String!, $stateId: String!) {
141
+ issueUpdate(id: $id, input: { stateId: $stateId }) { success }
142
+ }`,
143
+ { id: issueId, stateId },
144
+ );
145
+ }
146
+
147
+ async linkPullRequest(issueId: string, prUrl: string): Promise<void> {
148
+ await this.comment(issueId, `Aegis opened a PR: ${prUrl}`);
149
+ }
150
+
151
+ private async findStateId(name: string): Promise<string | undefined> {
152
+ const linear = this.requireConfig();
153
+ const result = await this.graphql<{ workflowStates: { nodes: LinearStateNode[] } }>(
154
+ `query WorkflowStates($teamId: String!) {
155
+ workflowStates(filter: { team: { id: { eq: $teamId } } }) {
156
+ nodes { id name }
157
+ }
158
+ }`,
159
+ { teamId: linear.teamId },
160
+ );
161
+ return result.workflowStates.nodes.find(
162
+ (state) => state.name.toLowerCase() === name.toLowerCase(),
163
+ )?.id;
164
+ }
165
+
166
+ private requireConfig() {
167
+ const linear = this.config.linear;
168
+ if (!linear?.apiKey || !linear.teamId) {
169
+ throw new Error("Linear API key and team ID are required");
170
+ }
171
+ return linear;
172
+ }
173
+
174
+ private async graphql<T>(query: string, variables: Record<string, unknown>): Promise<T> {
175
+ const linear = this.requireConfig();
176
+ const response = await fetch("https://api.linear.app/graphql", {
177
+ method: "POST",
178
+ headers: {
179
+ Authorization: linear.apiKey ?? "",
180
+ "Content-Type": "application/json",
181
+ },
182
+ body: JSON.stringify({ query, variables }),
183
+ });
184
+ const payload = (await response.json()) as { data?: T; errors?: Array<{ message: string }> };
185
+ if (!response.ok || payload.errors?.length) {
186
+ throw new Error(
187
+ `Linear GraphQL failed: ${response.status} ${
188
+ payload.errors?.map((error) => error.message).join("; ") ?? ""
189
+ }`,
190
+ );
191
+ }
192
+ if (!payload.data) throw new Error("Linear GraphQL returned no data");
193
+ return payload.data;
194
+ }
195
+ }
@@ -0,0 +1,48 @@
1
+ import type { AegisConfig } from "../shared/types.js";
2
+
3
+ export async function sendTelegramMessage(config: AegisConfig, text: string): Promise<void> {
4
+ const token = config.telegram?.botToken;
5
+ const chatId = config.telegram?.chatId;
6
+ if (!token || !chatId) return;
7
+
8
+ const response = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
9
+ method: "POST",
10
+ headers: { "Content-Type": "application/json" },
11
+ body: JSON.stringify({
12
+ chat_id: chatId,
13
+ text,
14
+ disable_web_page_preview: true,
15
+ }),
16
+ });
17
+
18
+ if (!response.ok) {
19
+ const body = await response.text();
20
+ throw new Error(`Telegram sendMessage failed: ${response.status} ${body}`);
21
+ }
22
+ }
23
+
24
+ export type TelegramCommand =
25
+ | { action: "proceed"; runId: string; text?: string; actor: string }
26
+ | { action: "stop"; runId: string; text?: string; actor: string }
27
+ | { action: "ask"; runId: string; text: string; actor: string }
28
+ | { action: "ignore"; actor: string };
29
+
30
+ export function parseTelegramUpdate(update: unknown): TelegramCommand {
31
+ const message = (
32
+ update as { message?: { text?: string; from?: { username?: string; id?: number } } }
33
+ ).message;
34
+ const actor = message?.from?.username ?? String(message?.from?.id ?? "telegram");
35
+ return parseAegisCommand(message?.text ?? "", actor);
36
+ }
37
+
38
+ export function parseAegisCommand(text: string, actor: string): TelegramCommand {
39
+ const trimmed = text.trim();
40
+ if (!trimmed.startsWith("/aegis")) return { action: "ignore", actor };
41
+ const [, command, runId, ...rest] = trimmed.split(/\s+/);
42
+ if (command === "proceed" && runId) return { action: "proceed", runId, actor };
43
+ if (command === "stop" && runId) return { action: "stop", runId, actor };
44
+ if (command === "ask" && runId) {
45
+ return { action: "ask", runId, text: rest.join(" ").trim(), actor };
46
+ }
47
+ return { action: "ignore", actor };
48
+ }
@@ -0,0 +1,53 @@
1
+ export async function verifyGitHubWebhook(
2
+ body: string,
3
+ signature: string | null | undefined,
4
+ secret: string | undefined,
5
+ ): Promise<boolean> {
6
+ if (!secret) return true;
7
+ if (!signature?.startsWith("sha256=")) return false;
8
+ const expected = signature.slice("sha256=".length);
9
+ const encoder = new TextEncoder();
10
+ const key = await crypto.subtle.importKey(
11
+ "raw",
12
+ encoder.encode(secret),
13
+ { name: "HMAC", hash: "SHA-256" },
14
+ false,
15
+ ["sign"],
16
+ );
17
+ const digest = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
18
+ const actual = [...new Uint8Array(digest)]
19
+ .map((byte) => byte.toString(16).padStart(2, "0"))
20
+ .join("");
21
+ return timingSafeEqual(actual, expected);
22
+ }
23
+
24
+ export async function verifyLinearWebhook(
25
+ body: string,
26
+ signature: string | null | undefined,
27
+ secret: string | undefined,
28
+ ): Promise<boolean> {
29
+ if (!secret) return true;
30
+ if (!signature) return false;
31
+ const encoder = new TextEncoder();
32
+ const key = await crypto.subtle.importKey(
33
+ "raw",
34
+ encoder.encode(secret),
35
+ { name: "HMAC", hash: "SHA-256" },
36
+ false,
37
+ ["sign"],
38
+ );
39
+ const digest = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
40
+ const actual = [...new Uint8Array(digest)]
41
+ .map((byte) => byte.toString(16).padStart(2, "0"))
42
+ .join("");
43
+ return timingSafeEqual(actual, signature);
44
+ }
45
+
46
+ function timingSafeEqual(a: string, b: string): boolean {
47
+ if (a.length !== b.length) return false;
48
+ let result = 0;
49
+ for (let i = 0; i < a.length; i++) {
50
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
51
+ }
52
+ return result === 0;
53
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * GitHub App JWT + installation token generation.
3
+ * Uses Web Crypto API (works in Cloudflare Workers).
4
+ */
5
+
6
+ export async function createAppJwt(appId: string, privateKey: string): Promise<string> {
7
+ const now = Math.floor(Date.now() / 1000);
8
+ const payload = {
9
+ iat: now - 60,
10
+ exp: now + 600,
11
+ iss: appId,
12
+ };
13
+
14
+ const header = { alg: "RS256", typ: "JWT" };
15
+ const encodedHeader = base64url(JSON.stringify(header));
16
+ const encodedPayload = base64url(JSON.stringify(payload));
17
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
18
+
19
+ const keyData = pemToArrayBuffer(privateKey);
20
+ const key = await crypto.subtle.importKey(
21
+ "pkcs8",
22
+ keyData,
23
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
24
+ false,
25
+ ["sign"],
26
+ );
27
+
28
+ const signature = await crypto.subtle.sign(
29
+ "RSASSA-PKCS1-v1_5",
30
+ key,
31
+ new TextEncoder().encode(signingInput),
32
+ );
33
+
34
+ return `${signingInput}.${base64url(signature)}`;
35
+ }
36
+
37
+ export async function getInstallationToken(
38
+ appId: string,
39
+ privateKey: string,
40
+ installationId: string,
41
+ ): Promise<string> {
42
+ const jwt = await createAppJwt(appId, privateKey);
43
+
44
+ const response = await fetch(
45
+ `https://api.github.com/app/installations/${installationId}/access_tokens`,
46
+ {
47
+ method: "POST",
48
+ headers: {
49
+ Authorization: `Bearer ${jwt}`,
50
+ Accept: "application/vnd.github+json",
51
+ "X-GitHub-Api-Version": "2022-11-28",
52
+ "User-Agent": "aegis-bot",
53
+ },
54
+ },
55
+ );
56
+
57
+ if (!response.ok) {
58
+ const text = await response.text();
59
+ throw new Error(`Failed to get installation token: ${response.status} ${text}`);
60
+ }
61
+
62
+ const data = (await response.json()) as { token: string };
63
+ return data.token;
64
+ }
65
+
66
+ function base64url(input: string | ArrayBuffer): string {
67
+ let base64: string;
68
+ if (typeof input === "string") {
69
+ base64 = btoa(input);
70
+ } else {
71
+ const bytes = new Uint8Array(input);
72
+ let binary = "";
73
+ for (const byte of bytes) {
74
+ binary += String.fromCharCode(byte);
75
+ }
76
+ base64 = btoa(binary);
77
+ }
78
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
79
+ }
80
+
81
+ function pemToArrayBuffer(pem: string): ArrayBuffer {
82
+ const lines = pem
83
+ .replace(/-----BEGIN .+-----/, "")
84
+ .replace(/-----END .+-----/, "")
85
+ .replace(/\s/g, "");
86
+ const binary = atob(lines);
87
+ const bytes = new Uint8Array(binary.length);
88
+ for (let i = 0; i < binary.length; i++) {
89
+ bytes[i] = binary.charCodeAt(i);
90
+ }
91
+ return bytes.buffer;
92
+ }
@@ -0,0 +1 @@
1
+ export { getInstallationToken, createAppJwt } from "./github-token.js";
@@ -0,0 +1,292 @@
1
+ import { Hono } from "hono";
2
+ import { startImplementationRun, startPlanRun } from "../agent/client.js";
3
+ import { pickupReadyBugs, scanReadyBugs } from "../core/pickup.js";
4
+ import { GitHubClient } from "../integrations/github.js";
5
+ import { LinearClient } from "../integrations/linear.js";
6
+ import {
7
+ parseAegisCommand,
8
+ parseTelegramUpdate,
9
+ sendTelegramMessage,
10
+ } from "../integrations/telegram.js";
11
+ import { verifyGitHubWebhook, verifyLinearWebhook } from "../integrations/webhooks.js";
12
+ import { configFromBindings } from "../shared/config.js";
13
+ import { formatReadyDecisionLine } from "../shared/format.js";
14
+ import { runFromItem } from "../shared/run-state.js";
15
+ import type { Bindings, ReadinessDecision, WorkItem, WorkRun } from "../shared/types.js";
16
+ import { sourceFor } from "../sources/index.js";
17
+
18
+ type HonoBindings = { Bindings: Bindings };
19
+
20
+ export function createAegisApp() {
21
+ const app = new Hono<HonoBindings>();
22
+
23
+ app.get("/", (c) => {
24
+ return c.json({ service: "aegis", status: "ok", product: "AFK bug-fixing agent" });
25
+ });
26
+
27
+ app.get("/api/pickup", async (c) => {
28
+ const config = { ...configFromBindings(c.env), workerUrl: new URL(c.req.url).origin };
29
+ const decisions = await scanReadyBugs(config);
30
+ return c.json({
31
+ dryRun: true,
32
+ count: decisions.length,
33
+ decisions: decisions.map((decision) => ({
34
+ source: decision.item.source,
35
+ identifier: decision.item.identifier,
36
+ title: decision.item.title,
37
+ url: decision.item.url,
38
+ action: decision.action,
39
+ reason: decision.decision.reason,
40
+ line: formatReadyDecisionLine(decision.item, decision.decision),
41
+ })),
42
+ });
43
+ });
44
+
45
+ app.post("/api/pickup", async (c) => {
46
+ const config = { ...configFromBindings(c.env), workerUrl: new URL(c.req.url).origin };
47
+ const decisions = await pickupReadyBugs({
48
+ config,
49
+ startRun: async (item, run, decision) => {
50
+ c.executionCtx.waitUntil(
51
+ startPlanSafely(c.env, item, run, decision, new URL(c.req.url).origin),
52
+ );
53
+ },
54
+ });
55
+ return c.json({
56
+ count: decisions.length,
57
+ decisions: decisions.map((decision) => ({
58
+ source: decision.item.source,
59
+ identifier: decision.item.identifier,
60
+ action: decision.action,
61
+ reason: decision.decision.reason,
62
+ runId: decision.run?.id,
63
+ })),
64
+ });
65
+ });
66
+
67
+ app.post("/api/runs/:id/proceed", async (c) => {
68
+ await proceedRun(c.env, c.req.param("id"), "api");
69
+ return c.json({ accepted: true, runId: c.req.param("id") });
70
+ });
71
+
72
+ app.post("/api/runs/:id/stop", async (c) => {
73
+ await stopRun(c.env, c.req.param("id"));
74
+ return c.json({ accepted: true, runId: c.req.param("id") });
75
+ });
76
+
77
+ app.post("/webhook/telegram", async (c) => {
78
+ const config = configFromBindings(c.env);
79
+ const secret = c.req.header("X-Telegram-Bot-Api-Secret-Token");
80
+ if (config.telegram?.webhookSecret && secret !== config.telegram.webhookSecret) {
81
+ return c.json({ error: "invalid secret" }, 401);
82
+ }
83
+ const command = parseTelegramUpdate(await c.req.json());
84
+ if (command.action === "ignore") return c.json({ ignored: true });
85
+ if (command.action === "proceed") {
86
+ await proceedRun(c.env, command.runId, command.actor);
87
+ return c.json({ accepted: true });
88
+ }
89
+ if (command.action === "stop") {
90
+ await stopRun(c.env, command.runId);
91
+ await sendTelegramMessage(config, `Stopped Aegis run ${command.runId}.`);
92
+ return c.json({ accepted: true });
93
+ }
94
+ await askRun(c.env, command.runId, command.text, command.actor);
95
+ return c.json({ accepted: true });
96
+ });
97
+
98
+ app.post("/webhook/github", async (c) => {
99
+ const config = configFromBindings(c.env);
100
+ const rawBody = await c.req.text();
101
+ const valid = await verifyGitHubWebhook(
102
+ rawBody,
103
+ c.req.header("X-Hub-Signature-256"),
104
+ config.github?.webhookSecret,
105
+ );
106
+ if (!valid) return c.json({ error: "invalid signature" }, 401);
107
+ const payload = JSON.parse(rawBody) as {
108
+ action?: string;
109
+ issue?: { number?: number };
110
+ comment?: { body?: string; user?: { login?: string } };
111
+ };
112
+ const event = c.req.header("X-GitHub-Event");
113
+ if (
114
+ event === "issues" &&
115
+ ["opened", "edited", "labeled", "unlabeled", "reopened"].includes(payload.action ?? "")
116
+ ) {
117
+ c.executionCtx.waitUntil(pickupFromWebhook(c.env, new URL(c.req.url).origin));
118
+ return c.json({ accepted: true, trigger: "pickup" });
119
+ }
120
+
121
+ const command = parseAegisCommand(
122
+ payload.comment?.body ?? "",
123
+ payload.comment?.user?.login ?? "github",
124
+ );
125
+ if (command.action === "ignore") return c.json({ ignored: true });
126
+ if (!payload.issue?.number) return c.json({ error: "missing issue" }, 400);
127
+ const item = await new GitHubClient(config).getIssue(
128
+ config.monitoredRepo,
129
+ payload.issue.number,
130
+ );
131
+ await handleCommand(c.env, command, item);
132
+ return c.json({ accepted: true });
133
+ });
134
+
135
+ app.post("/webhook/linear", async (c) => {
136
+ const config = configFromBindings(c.env);
137
+ const rawBody = await c.req.text();
138
+ const valid = await verifyLinearWebhook(
139
+ rawBody,
140
+ c.req.header("Linear-Signature"),
141
+ config.linear?.webhookSecret,
142
+ );
143
+ if (!valid) return c.json({ error: "invalid signature" }, 401);
144
+ const payload = JSON.parse(rawBody) as {
145
+ type?: string;
146
+ webhookTimestamp?: number;
147
+ actor?: { name?: string; email?: string };
148
+ data?: { body?: string; issue?: { id?: string } };
149
+ };
150
+ if (
151
+ payload.webhookTimestamp &&
152
+ Math.abs(Date.now() - payload.webhookTimestamp) > 5 * 60 * 1000
153
+ ) {
154
+ return c.json({ error: "stale webhook" }, 401);
155
+ }
156
+ if (payload.type !== "Comment") {
157
+ if (payload.type === "Issue") {
158
+ c.executionCtx.waitUntil(pickupFromWebhook(c.env, new URL(c.req.url).origin));
159
+ return c.json({ accepted: true, trigger: "pickup" });
160
+ }
161
+ return c.json({ ignored: true });
162
+ }
163
+ const command = parseAegisCommand(
164
+ payload.data?.body ?? "",
165
+ payload.actor?.name ?? payload.actor?.email ?? "linear",
166
+ );
167
+ if (command.action === "ignore") return c.json({ ignored: true });
168
+ if (!payload.data?.issue?.id) return c.json({ error: "missing issue" }, 400);
169
+ const item = await new LinearClient(config).getIssue(payload.data.issue.id);
170
+ await handleCommand(c.env, command, item);
171
+ return c.json({ accepted: true });
172
+ });
173
+
174
+ return app;
175
+ }
176
+
177
+ export async function scheduledPickup(
178
+ _controller: ScheduledController,
179
+ env: Bindings,
180
+ ctx: ExecutionContext,
181
+ ): Promise<void> {
182
+ const config = configFromBindings(env);
183
+ const decisions = await pickupReadyBugs({
184
+ config,
185
+ startRun: async (item, run, decision) => {
186
+ ctx.waitUntil(startPlanSafely(env, item, run, decision));
187
+ },
188
+ });
189
+ console.log(`[aegis] scheduled pickup checked ${decisions.length} item(s)`);
190
+ }
191
+
192
+ async function handleCommand(
193
+ env: Bindings,
194
+ command: ReturnType<typeof parseAegisCommand>,
195
+ item: WorkItem,
196
+ ): Promise<void> {
197
+ if (command.action === "proceed") {
198
+ await proceedRun(env, command.runId, command.actor, item);
199
+ return;
200
+ }
201
+ if (command.action === "stop") {
202
+ await stopRun(env, command.runId, item);
203
+ return;
204
+ }
205
+ if (command.action === "ask") {
206
+ await askRun(env, command.runId, command.text, command.actor, item);
207
+ }
208
+ }
209
+
210
+ async function startPlanSafely(
211
+ env: Bindings,
212
+ item: WorkItem,
213
+ run: WorkRun,
214
+ decision: ReadinessDecision,
215
+ workerUrl?: string,
216
+ ): Promise<void> {
217
+ const baseConfig = configFromBindings(env);
218
+ const config = { ...baseConfig, workerUrl: baseConfig.workerUrl ?? workerUrl };
219
+ try {
220
+ await startPlanRun({ config, item, run, decision });
221
+ } catch (error) {
222
+ const message = error instanceof Error ? error.message : String(error);
223
+ await sourceFor(config, item.source).markBlocked(item, `Aegis failed to run: ${message}`);
224
+ }
225
+ }
226
+
227
+ async function pickupFromWebhook(env: Bindings, origin: string): Promise<void> {
228
+ const config = {
229
+ ...configFromBindings(env),
230
+ workerUrl: configFromBindings(env).workerUrl ?? origin,
231
+ };
232
+ await pickupReadyBugs({
233
+ config,
234
+ startRun: async (item, run, decision) => {
235
+ await startPlanSafely(env, item, run, decision, origin);
236
+ },
237
+ });
238
+ }
239
+
240
+ async function proceedRun(
241
+ env: Bindings,
242
+ runId: string,
243
+ actor: string,
244
+ item?: WorkItem,
245
+ ): Promise<void> {
246
+ const config = configFromBindings(env);
247
+ const resolved = item ?? (await resolveItemFromRunId(config, runId));
248
+ const run = runFromItem(resolved, "plan-first");
249
+ await startImplementationRun({ config, item: resolved, run, approvedBy: actor });
250
+ }
251
+
252
+ async function askRun(
253
+ env: Bindings,
254
+ runId: string,
255
+ text: string,
256
+ actor: string,
257
+ item?: WorkItem,
258
+ ): Promise<void> {
259
+ if (!text.trim()) return;
260
+ const config = configFromBindings(env);
261
+ const resolved = item ?? (await resolveItemFromRunId(config, runId));
262
+ const followUpItem = {
263
+ ...resolved,
264
+ body: `${resolved.body}
265
+
266
+ Human follow-up from ${actor}:
267
+ ${text}`,
268
+ };
269
+ await startPlanRun({
270
+ config,
271
+ item: followUpItem,
272
+ run: runFromItem(resolved, "plan-first"),
273
+ decision: { ready: true, reason: "human follow-up", risk: "low", autoImplementAllowed: false },
274
+ });
275
+ }
276
+
277
+ async function stopRun(env: Bindings, runId: string, item?: WorkItem): Promise<void> {
278
+ const config = configFromBindings(env);
279
+ const resolved = item ?? (await resolveItemFromRunId(config, runId));
280
+ await sourceFor(config, resolved.source).markBlocked(resolved, `Aegis run ${runId} was stopped.`);
281
+ }
282
+
283
+ async function resolveItemFromRunId(
284
+ config: ReturnType<typeof configFromBindings>,
285
+ runId: string,
286
+ ): Promise<WorkItem> {
287
+ const match = runId.match(/^gh-.+-(\d+)$/);
288
+ if (match) {
289
+ return new GitHubClient(config).getIssue(config.monitoredRepo, Number(match[1]));
290
+ }
291
+ return new LinearClient(config).getIssue(runId.toUpperCase());
292
+ }