heyhank 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/README.md +40 -0
  2. package/bin/cli.ts +168 -0
  3. package/bin/ctl.ts +528 -0
  4. package/bin/generate-token.ts +28 -0
  5. package/dist/apple-touch-icon.png +0 -0
  6. package/dist/assets/AgentsPage-BPhirnCe.js +7 -0
  7. package/dist/assets/AssistantPage-DJ-cMQfb.js +1 -0
  8. package/dist/assets/CronManager-DDbz-yiT.js +1 -0
  9. package/dist/assets/HelpPage-DMfkzERp.js +1 -0
  10. package/dist/assets/IntegrationsPage-CrOitCmJ.js +1 -0
  11. package/dist/assets/MediaPage-CE5rdvkC.js +1 -0
  12. package/dist/assets/PlatformDashboard-Do6F0O2p.js +1 -0
  13. package/dist/assets/Playground-Fc5cdc5p.js +109 -0
  14. package/dist/assets/ProcessPanel-CslEiZkI.js +2 -0
  15. package/dist/assets/PromptsPage-D2EhsdNO.js +4 -0
  16. package/dist/assets/RunsPage-C5BZF5Rx.js +1 -0
  17. package/dist/assets/SandboxManager-a1AVI5q2.js +8 -0
  18. package/dist/assets/SettingsPage-DirhjQrJ.js +51 -0
  19. package/dist/assets/SocialMediaPage-DBuM28vD.js +1 -0
  20. package/dist/assets/TailscalePage-CHiFhZXF.js +1 -0
  21. package/dist/assets/TelephonyPage-x0VV0fOo.js +1 -0
  22. package/dist/assets/TerminalPage-Drwyrnfd.js +1 -0
  23. package/dist/assets/gemini-audio-t-TSU-To.js +17 -0
  24. package/dist/assets/gemini-live-client-C7rqAW7G.js +166 -0
  25. package/dist/assets/index-C8M_PUmX.css +32 -0
  26. package/dist/assets/index-CEqZnThB.js +204 -0
  27. package/dist/assets/sw-register-LSSpj6RU.js +1 -0
  28. package/dist/assets/time-ago-B6r_l9u1.js +1 -0
  29. package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
  30. package/dist/favicon-32-original.png +0 -0
  31. package/dist/favicon-32.png +0 -0
  32. package/dist/favicon.ico +0 -0
  33. package/dist/favicon.svg +8 -0
  34. package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
  35. package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
  36. package/dist/heyhank-mascot-poster.png +0 -0
  37. package/dist/heyhank-mascot.mp4 +0 -0
  38. package/dist/heyhank-mascot.webm +0 -0
  39. package/dist/icon-192-original.png +0 -0
  40. package/dist/icon-192.png +0 -0
  41. package/dist/icon-512-original.png +0 -0
  42. package/dist/icon-512.png +0 -0
  43. package/dist/index.html +21 -0
  44. package/dist/logo-192.png +0 -0
  45. package/dist/logo-512.png +0 -0
  46. package/dist/logo-codex.svg +14 -0
  47. package/dist/logo-docker.svg +4 -0
  48. package/dist/logo-original.png +0 -0
  49. package/dist/logo.png +0 -0
  50. package/dist/logo.svg +14 -0
  51. package/dist/manifest.json +24 -0
  52. package/dist/push-sw.js +34 -0
  53. package/dist/sw.js +1 -0
  54. package/dist/workbox-d2a0910a.js +1 -0
  55. package/package.json +109 -0
  56. package/server/agent-cron-migrator.ts +85 -0
  57. package/server/agent-executor.ts +357 -0
  58. package/server/agent-store.ts +185 -0
  59. package/server/agent-timeout.ts +107 -0
  60. package/server/agent-types.ts +122 -0
  61. package/server/ai-validation-settings.ts +37 -0
  62. package/server/ai-validator.ts +181 -0
  63. package/server/anthropic-provider-migration.ts +48 -0
  64. package/server/assistant-store.ts +272 -0
  65. package/server/auth-manager.ts +150 -0
  66. package/server/auto-approve.ts +153 -0
  67. package/server/auto-namer.ts +36 -0
  68. package/server/backend-adapter.ts +54 -0
  69. package/server/cache-headers.ts +61 -0
  70. package/server/calendar-service.ts +434 -0
  71. package/server/claude-adapter.ts +889 -0
  72. package/server/claude-container-auth.ts +30 -0
  73. package/server/claude-session-discovery.ts +157 -0
  74. package/server/claude-session-history.ts +410 -0
  75. package/server/cli-launcher.ts +1303 -0
  76. package/server/codex-adapter.ts +3027 -0
  77. package/server/codex-container-auth.ts +24 -0
  78. package/server/codex-home.ts +27 -0
  79. package/server/codex-ws-proxy.cjs +226 -0
  80. package/server/commands-discovery.ts +81 -0
  81. package/server/constants.ts +7 -0
  82. package/server/container-manager.ts +1053 -0
  83. package/server/cost-tracker.ts +222 -0
  84. package/server/cron-scheduler.ts +243 -0
  85. package/server/cron-store.ts +148 -0
  86. package/server/cron-types.ts +63 -0
  87. package/server/email-service.ts +354 -0
  88. package/server/env-manager.ts +161 -0
  89. package/server/event-bus-types.ts +75 -0
  90. package/server/event-bus.ts +124 -0
  91. package/server/execution-store.ts +170 -0
  92. package/server/federation/node-connection.ts +190 -0
  93. package/server/federation/node-manager.ts +366 -0
  94. package/server/federation/node-store.ts +86 -0
  95. package/server/federation/node-types.ts +121 -0
  96. package/server/fs-utils.ts +15 -0
  97. package/server/git-utils.ts +421 -0
  98. package/server/github-pr.ts +379 -0
  99. package/server/google-media.ts +342 -0
  100. package/server/image-pull-manager.ts +279 -0
  101. package/server/index.ts +491 -0
  102. package/server/internal-ai.ts +237 -0
  103. package/server/kill-switch.ts +99 -0
  104. package/server/llm-providers.ts +342 -0
  105. package/server/logger.ts +259 -0
  106. package/server/mcp-registry.ts +401 -0
  107. package/server/message-bus.ts +271 -0
  108. package/server/message-delivery.ts +128 -0
  109. package/server/metrics-collector.ts +350 -0
  110. package/server/metrics-types.ts +108 -0
  111. package/server/middleware/managed-auth.ts +195 -0
  112. package/server/novnc-proxy.ts +99 -0
  113. package/server/path-resolver.ts +186 -0
  114. package/server/paths.ts +13 -0
  115. package/server/pr-poller.ts +162 -0
  116. package/server/prompt-manager.ts +211 -0
  117. package/server/protocol/claude-upstream/README.md +19 -0
  118. package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
  119. package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
  120. package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
  121. package/server/protocol/codex-upstream/README.md +18 -0
  122. package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
  123. package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
  124. package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
  125. package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
  126. package/server/protocol-monitor.ts +50 -0
  127. package/server/provider-manager.ts +111 -0
  128. package/server/provider-registry.ts +393 -0
  129. package/server/push-notifications.ts +221 -0
  130. package/server/recorder.ts +374 -0
  131. package/server/recording-hub/compat-validator.ts +284 -0
  132. package/server/recording-hub/diagnostics.ts +299 -0
  133. package/server/recording-hub/hub-config.ts +19 -0
  134. package/server/recording-hub/hub-routes.ts +236 -0
  135. package/server/recording-hub/hub-store.ts +265 -0
  136. package/server/recording-hub/replay-adapter.ts +207 -0
  137. package/server/relay-client.ts +320 -0
  138. package/server/reminder-scheduler.ts +38 -0
  139. package/server/replay.ts +78 -0
  140. package/server/routes/agent-routes.ts +264 -0
  141. package/server/routes/assistant-routes.ts +90 -0
  142. package/server/routes/cron-routes.ts +103 -0
  143. package/server/routes/env-routes.ts +95 -0
  144. package/server/routes/federation-routes.ts +76 -0
  145. package/server/routes/fs-routes.ts +622 -0
  146. package/server/routes/git-routes.ts +97 -0
  147. package/server/routes/llm-routes.ts +166 -0
  148. package/server/routes/media-routes.ts +135 -0
  149. package/server/routes/metrics-routes.ts +13 -0
  150. package/server/routes/platform-routes.ts +1379 -0
  151. package/server/routes/prompt-routes.ts +67 -0
  152. package/server/routes/provider-routes.ts +109 -0
  153. package/server/routes/sandbox-routes.ts +127 -0
  154. package/server/routes/settings-routes.ts +285 -0
  155. package/server/routes/skills-routes.ts +100 -0
  156. package/server/routes/socialmedia-routes.ts +208 -0
  157. package/server/routes/system-routes.ts +228 -0
  158. package/server/routes/tailscale-routes.ts +22 -0
  159. package/server/routes/telephony-routes.ts +259 -0
  160. package/server/routes.ts +1379 -0
  161. package/server/sandbox-manager.ts +168 -0
  162. package/server/service.ts +718 -0
  163. package/server/session-creation-service.ts +457 -0
  164. package/server/session-git-info.ts +104 -0
  165. package/server/session-names.ts +67 -0
  166. package/server/session-orchestrator.ts +824 -0
  167. package/server/session-state-machine.ts +207 -0
  168. package/server/session-store.ts +146 -0
  169. package/server/session-types.ts +511 -0
  170. package/server/settings-manager.ts +149 -0
  171. package/server/shared-context.ts +157 -0
  172. package/server/socialmedia/adapter.ts +15 -0
  173. package/server/socialmedia/adapters/ayrshare-adapter.ts +169 -0
  174. package/server/socialmedia/adapters/buffer-adapter.ts +299 -0
  175. package/server/socialmedia/adapters/postiz-adapter.ts +298 -0
  176. package/server/socialmedia/manager.ts +227 -0
  177. package/server/socialmedia/store.ts +98 -0
  178. package/server/socialmedia/types.ts +89 -0
  179. package/server/tailscale-manager.ts +451 -0
  180. package/server/telephony/audio-bridge.ts +331 -0
  181. package/server/telephony/call-manager.ts +457 -0
  182. package/server/telephony/call-types.ts +108 -0
  183. package/server/telephony/telephony-store.ts +119 -0
  184. package/server/terminal-manager.ts +240 -0
  185. package/server/update-checker.ts +192 -0
  186. package/server/usage-limits.ts +225 -0
  187. package/server/web-push.d.ts +51 -0
  188. package/server/worktree-tracker.ts +84 -0
  189. package/server/ws-auth.ts +41 -0
  190. package/server/ws-bridge-browser-ingest.ts +72 -0
  191. package/server/ws-bridge-browser.ts +112 -0
  192. package/server/ws-bridge-cli-ingest.ts +81 -0
  193. package/server/ws-bridge-codex.ts +266 -0
  194. package/server/ws-bridge-controls.ts +20 -0
  195. package/server/ws-bridge-persist.ts +66 -0
  196. package/server/ws-bridge-publish.ts +79 -0
  197. package/server/ws-bridge-replay.ts +61 -0
  198. package/server/ws-bridge-types.ts +121 -0
  199. package/server/ws-bridge.ts +1240 -0
@@ -0,0 +1,379 @@
1
+ import { execFileSync, execSync } from "node:child_process";
2
+
3
+ // ─── Types ───────────────────────────────────────────────────────────────────
4
+
5
+ export interface GitHubCheckStatus {
6
+ name: string;
7
+ status: string;
8
+ conclusion: string | null;
9
+ }
10
+
11
+ export interface GitHubPRInfo {
12
+ number: number;
13
+ title: string;
14
+ url: string;
15
+ state: "OPEN" | "CLOSED" | "MERGED";
16
+ isDraft: boolean;
17
+ reviewDecision: "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | null;
18
+ additions: number;
19
+ deletions: number;
20
+ changedFiles: number;
21
+ checks: GitHubCheckStatus[];
22
+ checksSummary: {
23
+ total: number;
24
+ success: number;
25
+ failure: number;
26
+ pending: number;
27
+ };
28
+ reviewThreads: {
29
+ total: number;
30
+ resolved: number;
31
+ unresolved: number;
32
+ };
33
+ }
34
+
35
+ export interface PRStatusResponse {
36
+ available: boolean;
37
+ pr: GitHubPRInfo | null;
38
+ }
39
+
40
+ // ─── gh CLI Detection ────────────────────────────────────────────────────────
41
+
42
+ let _ghAvailable: boolean | null = null;
43
+
44
+ export function isGhAvailable(): boolean {
45
+ if (_ghAvailable !== null) return _ghAvailable;
46
+ try {
47
+ execSync("which gh", { stdio: "pipe", timeout: 5_000 });
48
+ _ghAvailable = true;
49
+ } catch {
50
+ _ghAvailable = false;
51
+ }
52
+ return _ghAvailable;
53
+ }
54
+
55
+ // Exported for testing
56
+ export function _resetGhAvailable() {
57
+ _ghAvailable = null;
58
+ }
59
+
60
+ // ─── Repo Slug Resolution ────────────────────────────────────────────────────
61
+
62
+ const repoSlugCache = new Map<string, { slug: string | null; timestamp: number }>();
63
+ const REPO_SLUG_TTL = 5 * 60_000; // 5 minutes
64
+
65
+ function getRepoSlug(cwd: string): string | null {
66
+ const cached = repoSlugCache.get(cwd);
67
+ if (cached && Date.now() - cached.timestamp < REPO_SLUG_TTL) {
68
+ return cached.slug;
69
+ }
70
+ try {
71
+ const slug = execSync("gh repo view --json nameWithOwner --jq .nameWithOwner", {
72
+ cwd,
73
+ stdio: "pipe",
74
+ timeout: 10_000,
75
+ })
76
+ .toString()
77
+ .trim();
78
+ const result = slug || null;
79
+ repoSlugCache.set(cwd, { slug: result, timestamp: Date.now() });
80
+ return result;
81
+ } catch {
82
+ repoSlugCache.set(cwd, { slug: null, timestamp: Date.now() });
83
+ return null;
84
+ }
85
+ }
86
+
87
+ async function getRepoSlugAsync(cwd: string): Promise<string | null> {
88
+ const cached = repoSlugCache.get(cwd);
89
+ if (cached && Date.now() - cached.timestamp < REPO_SLUG_TTL) {
90
+ return cached.slug;
91
+ }
92
+ try {
93
+ const proc = Bun.spawn(
94
+ ["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"],
95
+ { cwd, stdout: "pipe", stderr: "pipe" },
96
+ );
97
+ const timeout = setTimeout(() => proc.kill(), 10_000);
98
+ const exitCode = await proc.exited;
99
+ clearTimeout(timeout);
100
+ if (exitCode !== 0) {
101
+ repoSlugCache.set(cwd, { slug: null, timestamp: Date.now() });
102
+ return null;
103
+ }
104
+ const slug = (await new Response(proc.stdout).text()).trim();
105
+ const result = slug || null;
106
+ repoSlugCache.set(cwd, { slug: result, timestamp: Date.now() });
107
+ return result;
108
+ } catch {
109
+ repoSlugCache.set(cwd, { slug: null, timestamp: Date.now() });
110
+ return null;
111
+ }
112
+ }
113
+
114
+ // ─── PR Data Cache ───────────────────────────────────────────────────────────
115
+
116
+ const prCache = new Map<string, { data: GitHubPRInfo | null; timestamp: number; ttl: number }>();
117
+ const PR_CACHE_TTL = 30_000; // 30 seconds (default / legacy)
118
+
119
+ // Exported for testing
120
+ export function _clearCaches() {
121
+ prCache.clear();
122
+ repoSlugCache.clear();
123
+ _ghAvailable = null;
124
+ }
125
+
126
+ // ─── Adaptive TTL ───────────────────────────────────────────────────────────
127
+
128
+ /** Compute polling interval based on PR state. */
129
+ export function computeAdaptiveTTL(pr: GitHubPRInfo | null): number {
130
+ if (!pr) return 60_000; // No PR found — check again in 60s
131
+
132
+ // Merged or closed — terminal state, rarely changes
133
+ if (pr.state === "MERGED" || pr.state === "CLOSED") return 300_000; // 5 minutes
134
+
135
+ // CI actively running (pending checks) — user is watching
136
+ if (pr.checksSummary.pending > 0) return 10_000; // 10 seconds
137
+
138
+ // CI failed — user likely pushing fixes
139
+ if (pr.checksSummary.failure > 0) return 30_000; // 30 seconds
140
+
141
+ // Changes requested — moderate frequency
142
+ if (pr.reviewDecision === "CHANGES_REQUESTED") return 30_000; // 30 seconds
143
+
144
+ // Approved, all checks passed — stable
145
+ if (pr.reviewDecision === "APPROVED" && pr.checksSummary.pending === 0) return 120_000; // 2 minutes
146
+
147
+ // Review pending, checks passed — waiting on human reviewer
148
+ if (pr.reviewDecision === "REVIEW_REQUIRED" || pr.reviewDecision === null) return 45_000; // 45 seconds
149
+
150
+ // Default fallback
151
+ return 30_000;
152
+ }
153
+
154
+ // ─── GraphQL Query ───────────────────────────────────────────────────────────
155
+
156
+ const PR_QUERY = `
157
+ query($owner: String!, $name: String!, $branch: String!) {
158
+ repository(owner: $owner, name: $name) {
159
+ pullRequests(headRefName: $branch, first: 5, orderBy: {field: UPDATED_AT, direction: DESC}, states: [OPEN, MERGED]) {
160
+ nodes {
161
+ number
162
+ title
163
+ url
164
+ state
165
+ isDraft
166
+ isCrossRepository
167
+ reviewDecision
168
+ additions
169
+ deletions
170
+ changedFiles
171
+ reviewThreads(first: 100) {
172
+ totalCount
173
+ nodes {
174
+ isResolved
175
+ }
176
+ }
177
+ commits(last: 1) {
178
+ nodes {
179
+ commit {
180
+ statusCheckRollup {
181
+ contexts(first: 50) {
182
+ nodes {
183
+ __typename
184
+ ... on CheckRun {
185
+ name
186
+ status
187
+ conclusion
188
+ }
189
+ ... on StatusContext {
190
+ context
191
+ state
192
+ }
193
+ }
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }
202
+ }`;
203
+
204
+ // ─── Response Parsing ────────────────────────────────────────────────────────
205
+
206
+ interface GraphQLCheckRunNode {
207
+ __typename: "CheckRun";
208
+ name: string;
209
+ status: string;
210
+ conclusion: string | null;
211
+ }
212
+
213
+ interface GraphQLStatusContextNode {
214
+ __typename: "StatusContext";
215
+ context: string;
216
+ state: string;
217
+ }
218
+
219
+ type GraphQLContextNode = GraphQLCheckRunNode | GraphQLStatusContextNode;
220
+
221
+ export function parseGraphQLResponse(data: unknown): GitHubPRInfo | null {
222
+ try {
223
+ const repo = (data as any)?.data?.repository;
224
+ const nodes = repo?.pullRequests?.nodes;
225
+ if (!nodes || nodes.length === 0) return null;
226
+
227
+ // Filter out cross-repository (fork) PRs — we only want same-repo PRs
228
+ const sameRepoPRs = nodes.filter((n: any) => !n.isCrossRepository);
229
+ if (sameRepoPRs.length === 0) return null;
230
+
231
+ const pr = sameRepoPRs[0];
232
+
233
+ // Normalize checks
234
+ const rawContexts: GraphQLContextNode[] =
235
+ pr.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes ?? [];
236
+
237
+ const checks: GitHubCheckStatus[] = rawContexts.map((node) => {
238
+ if (node.__typename === "CheckRun") {
239
+ return {
240
+ name: node.name,
241
+ status: node.status,
242
+ conclusion: node.conclusion,
243
+ };
244
+ }
245
+ // StatusContext
246
+ return {
247
+ name: node.context,
248
+ status: node.state === "PENDING" ? "IN_PROGRESS" : "COMPLETED",
249
+ conclusion: node.state === "SUCCESS" ? "SUCCESS" : (node.state === "FAILURE" || node.state === "ERROR") ? "FAILURE" : null,
250
+ };
251
+ });
252
+
253
+ // Compute summary
254
+ let success = 0;
255
+ let failure = 0;
256
+ let pending = 0;
257
+ for (const check of checks) {
258
+ if (check.conclusion === "SUCCESS" || check.conclusion === "NEUTRAL" || check.conclusion === "SKIPPED") {
259
+ success++;
260
+ } else if (check.conclusion === "FAILURE" || check.conclusion === "CANCELLED" || check.conclusion === "TIMED_OUT") {
261
+ failure++;
262
+ } else {
263
+ pending++;
264
+ }
265
+ }
266
+
267
+ // Compute review threads
268
+ const threadNodes: { isResolved: boolean }[] = pr.reviewThreads?.nodes ?? [];
269
+ const resolved = threadNodes.filter((t) => t.isResolved).length;
270
+ const unresolved = threadNodes.filter((t) => !t.isResolved).length;
271
+
272
+ return {
273
+ number: pr.number,
274
+ title: pr.title,
275
+ url: pr.url,
276
+ state: pr.state,
277
+ isDraft: pr.isDraft ?? false,
278
+ reviewDecision: pr.reviewDecision || null,
279
+ additions: pr.additions ?? 0,
280
+ deletions: pr.deletions ?? 0,
281
+ changedFiles: pr.changedFiles ?? 0,
282
+ checks,
283
+ checksSummary: { total: checks.length, success, failure, pending },
284
+ reviewThreads: {
285
+ total: pr.reviewThreads?.totalCount ?? 0,
286
+ resolved,
287
+ unresolved,
288
+ },
289
+ };
290
+ } catch {
291
+ return null;
292
+ }
293
+ }
294
+
295
+ // ─── Main Fetch Function (sync — legacy, used by tests) ─────────────────────
296
+
297
+ export async function fetchPRInfo(cwd: string, branch: string): Promise<GitHubPRInfo | null> {
298
+ if (!isGhAvailable()) return null;
299
+
300
+ const cacheKey = `${cwd}:${branch}`;
301
+ const cached = prCache.get(cacheKey);
302
+ if (cached && Date.now() - cached.timestamp < cached.ttl) {
303
+ return cached.data;
304
+ }
305
+
306
+ const slug = getRepoSlug(cwd);
307
+ if (!slug) {
308
+ prCache.set(cacheKey, { data: null, timestamp: Date.now(), ttl: PR_CACHE_TTL });
309
+ return null;
310
+ }
311
+
312
+ const [owner, name] = slug.split("/");
313
+ if (!owner || !name) return null;
314
+
315
+ try {
316
+ const result = execFileSync(
317
+ "gh",
318
+ ["api", "graphql", "-f", `query=${PR_QUERY}`, "-f", `owner=${owner}`, "-f", `name=${name}`, "-f", `branch=${branch}`],
319
+ { cwd, stdio: "pipe", timeout: 15_000 },
320
+ )
321
+ .toString()
322
+ .trim();
323
+
324
+ const parsed = JSON.parse(result);
325
+ const prInfo = parseGraphQLResponse(parsed);
326
+ const ttl = computeAdaptiveTTL(prInfo);
327
+ prCache.set(cacheKey, { data: prInfo, timestamp: Date.now(), ttl });
328
+ return prInfo;
329
+ } catch {
330
+ prCache.set(cacheKey, { data: null, timestamp: Date.now(), ttl: PR_CACHE_TTL });
331
+ return null;
332
+ }
333
+ }
334
+
335
+ // ─── Async Fetch Function (non-blocking, uses Bun.spawn) ────────────────────
336
+
337
+ export async function fetchPRInfoAsync(cwd: string, branch: string): Promise<GitHubPRInfo | null> {
338
+ if (!isGhAvailable()) return null;
339
+
340
+ const cacheKey = `${cwd}:${branch}`;
341
+ const cached = prCache.get(cacheKey);
342
+ if (cached && Date.now() - cached.timestamp < cached.ttl) {
343
+ return cached.data;
344
+ }
345
+
346
+ const slug = await getRepoSlugAsync(cwd);
347
+ if (!slug) {
348
+ prCache.set(cacheKey, { data: null, timestamp: Date.now(), ttl: 60_000 });
349
+ return null;
350
+ }
351
+
352
+ const [owner, name] = slug.split("/");
353
+ if (!owner || !name) return null;
354
+
355
+ try {
356
+ const proc = Bun.spawn(
357
+ ["gh", "api", "graphql", "-f", `query=${PR_QUERY}`, "-f", `owner=${owner}`, "-f", `name=${name}`, "-f", `branch=${branch}`],
358
+ { cwd, stdout: "pipe", stderr: "pipe" },
359
+ );
360
+ const timeout = setTimeout(() => proc.kill(), 15_000);
361
+ const exitCode = await proc.exited;
362
+ clearTimeout(timeout);
363
+
364
+ if (exitCode !== 0) {
365
+ prCache.set(cacheKey, { data: null, timestamp: Date.now(), ttl: PR_CACHE_TTL });
366
+ return null;
367
+ }
368
+
369
+ const stdout = (await new Response(proc.stdout).text()).trim();
370
+ const parsed = JSON.parse(stdout);
371
+ const prInfo = parseGraphQLResponse(parsed);
372
+ const ttl = computeAdaptiveTTL(prInfo);
373
+ prCache.set(cacheKey, { data: prInfo, timestamp: Date.now(), ttl });
374
+ return prInfo;
375
+ } catch {
376
+ prCache.set(cacheKey, { data: null, timestamp: Date.now(), ttl: PR_CACHE_TTL });
377
+ return null;
378
+ }
379
+ }