@nghyane/arcane 0.1.13 → 0.1.14

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 (323) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/package.json +21 -70
  3. package/scripts/format-prompts.ts +1 -3
  4. package/src/cli/args.ts +2 -7
  5. package/src/cli/config-cli.ts +1 -1
  6. package/src/cli/plugin-cli.ts +1 -1
  7. package/src/cli/setup-cli.ts +1 -1
  8. package/src/cli/update-cli.ts +1 -1
  9. package/src/cli/web-search-cli.ts +1 -1
  10. package/src/cli.ts +0 -1
  11. package/src/commands/config.ts +1 -1
  12. package/src/commands/grep.ts +1 -1
  13. package/src/commands/jupyter.ts +1 -1
  14. package/src/commands/plugin.ts +1 -1
  15. package/src/commands/setup.ts +1 -1
  16. package/src/commands/shell.ts +1 -1
  17. package/src/commands/ssh.ts +1 -1
  18. package/src/commands/stats.ts +1 -1
  19. package/src/commands/update.ts +1 -1
  20. package/src/config/model-registry.ts +3 -4
  21. package/src/config/model-resolver.ts +36 -9
  22. package/src/config/prompt-templates.ts +1 -9
  23. package/src/config/settings-schema.ts +32 -88
  24. package/src/config/settings.ts +3 -4
  25. package/src/debug/index.ts +1 -1
  26. package/src/debug/log-formatting.ts +1 -1
  27. package/src/debug/log-viewer.ts +2 -2
  28. package/src/discovery/helpers.ts +13 -3
  29. package/src/exa/index.ts +1 -35
  30. package/src/exa/render.ts +30 -190
  31. package/src/export/html/index.ts +1 -1
  32. package/src/extensibility/custom-tools/loader.ts +1 -1
  33. package/src/extensibility/custom-tools/types.ts +5 -1
  34. package/src/extensibility/custom-tools/wrapper.ts +1 -1
  35. package/src/extensibility/extensions/runner.ts +1 -1
  36. package/src/extensibility/extensions/types.ts +1 -1
  37. package/src/extensibility/extensions/wrapper.ts +7 -15
  38. package/src/extensibility/hooks/runner.ts +1 -1
  39. package/src/extensibility/hooks/types.ts +1 -1
  40. package/src/extensibility/plugins/doctor.ts +1 -1
  41. package/src/index.ts +13 -13
  42. package/src/lsp/index.ts +77 -24
  43. package/src/lsp/render.ts +34 -583
  44. package/src/lsp/types.ts +3 -3
  45. package/src/lsp/utils.ts +1 -1
  46. package/src/main.ts +1 -1
  47. package/src/mcp/tool-bridge.ts +1 -24
  48. package/src/modes/components/assistant-message.ts +7 -7
  49. package/src/modes/components/bash-execution.ts +48 -113
  50. package/src/modes/components/bordered-loader.ts +1 -1
  51. package/src/modes/components/branch-summary-message.ts +13 -10
  52. package/src/modes/components/compaction-summary-message.ts +14 -13
  53. package/src/modes/components/context-group.ts +106 -0
  54. package/src/modes/components/custom-message.ts +4 -5
  55. package/src/modes/components/diff.ts +2 -2
  56. package/src/modes/components/dynamic-border.ts +1 -1
  57. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  58. package/src/modes/components/extensions/extension-list.ts +1 -1
  59. package/src/modes/components/extensions/inspector-panel.ts +1 -1
  60. package/src/modes/components/footer.ts +2 -2
  61. package/src/modes/components/history-search.ts +1 -1
  62. package/src/modes/components/hook-editor.ts +1 -1
  63. package/src/modes/components/hook-input.ts +1 -1
  64. package/src/modes/components/hook-message.ts +4 -5
  65. package/src/modes/components/hook-selector.ts +1 -1
  66. package/src/modes/components/index.ts +0 -2
  67. package/src/modes/components/keybinding-hints.ts +1 -1
  68. package/src/modes/components/login-dialog.ts +1 -1
  69. package/src/modes/components/mcp-add-wizard.ts +1 -1
  70. package/src/modes/components/model-selector.ts +1 -1
  71. package/src/modes/components/oauth-selector.ts +1 -1
  72. package/src/modes/components/plugin-settings.ts +1 -1
  73. package/src/modes/components/python-execution.ts +49 -92
  74. package/src/modes/components/queue-mode-selector.ts +1 -1
  75. package/src/modes/components/session-selector.ts +1 -1
  76. package/src/modes/components/settings-defs.ts +5 -10
  77. package/src/modes/components/settings-selector.ts +1 -1
  78. package/src/modes/components/show-images-selector.ts +1 -1
  79. package/src/modes/components/skill-message.ts +4 -4
  80. package/src/modes/components/status-line/segments.ts +2 -2
  81. package/src/modes/components/status-line/separators.ts +1 -1
  82. package/src/modes/components/status-line-segment-editor.ts +1 -1
  83. package/src/modes/components/status-line.ts +1 -1
  84. package/src/modes/components/theme-selector.ts +1 -1
  85. package/src/modes/components/thinking-selector.ts +1 -1
  86. package/src/modes/components/todo-display.ts +2 -4
  87. package/src/modes/components/todo-reminder.ts +4 -4
  88. package/src/modes/components/tool-execution.ts +118 -440
  89. package/src/modes/components/tool-image-display.ts +107 -0
  90. package/src/modes/components/tree-selector.ts +2 -2
  91. package/src/modes/components/ttsr-notification.ts +4 -17
  92. package/src/modes/components/user-message-selector.ts +1 -1
  93. package/src/modes/components/user-message.ts +9 -10
  94. package/src/modes/components/welcome.ts +1 -1
  95. package/src/modes/controllers/command-controller.ts +1 -1
  96. package/src/modes/controllers/event-controller.ts +58 -187
  97. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  98. package/src/modes/controllers/input-controller.ts +3 -1
  99. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  100. package/src/modes/controllers/selector-controller.ts +3 -26
  101. package/src/modes/controllers/ssh-command-controller.ts +1 -1
  102. package/src/modes/interactive-mode.ts +3 -7
  103. package/src/modes/print-mode.ts +5 -5
  104. package/src/modes/rpc/rpc-mode.ts +1 -1
  105. package/src/modes/types.ts +1 -2
  106. package/src/modes/utils/ui-helpers.ts +34 -32
  107. package/src/patch/edit-tool.ts +742 -0
  108. package/src/patch/index.ts +32 -898
  109. package/src/patch/schemas.ts +208 -0
  110. package/src/patch/shared.ts +83 -151
  111. package/src/prompts/agents/explore.md +22 -37
  112. package/src/prompts/agents/frontmatter.md +1 -1
  113. package/src/prompts/agents/init.md +2 -2
  114. package/src/prompts/agents/librarian.md +30 -21
  115. package/src/prompts/agents/oracle.md +9 -2
  116. package/src/prompts/agents/reviewer.md +15 -49
  117. package/src/prompts/agents/task.md +17 -9
  118. package/src/prompts/compaction/branch-summary-context.md +1 -1
  119. package/src/prompts/compaction/branch-summary-preamble.md +1 -1
  120. package/src/prompts/compaction/branch-summary.md +4 -1
  121. package/src/prompts/compaction/compaction-short-summary.md +1 -1
  122. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  123. package/src/prompts/compaction/compaction-summary.md +4 -1
  124. package/src/prompts/compaction/compaction-turn-prefix.md +1 -1
  125. package/src/prompts/compaction/compaction-update-summary.md +1 -1
  126. package/src/prompts/memories/consolidation.md +1 -1
  127. package/src/prompts/memories/read_path.md +1 -1
  128. package/src/prompts/memories/stage_one_input.md +1 -1
  129. package/src/prompts/memories/stage_one_system.md +1 -1
  130. package/src/prompts/review-request.md +1 -1
  131. package/src/prompts/system/agent-creation-architect.md +1 -1
  132. package/src/prompts/system/agent-creation-user.md +1 -1
  133. package/src/prompts/system/custom-system-prompt.md +1 -1
  134. package/src/prompts/system/file-operations.md +1 -1
  135. package/src/prompts/system/subagent-system-prompt.md +2 -2
  136. package/src/prompts/system/summarization-system.md +1 -1
  137. package/src/prompts/system/system-prompt.md +163 -178
  138. package/src/prompts/system/title-system.md +1 -1
  139. package/src/prompts/system/ttsr-interrupt.md +1 -1
  140. package/src/prompts/system/verification-reminder.md +6 -0
  141. package/src/prompts/system/web-search.md +1 -1
  142. package/src/sdk.ts +0 -9
  143. package/src/session/agent-session.ts +244 -1459
  144. package/src/session/model-controller.ts +406 -0
  145. package/src/session/retry-utils.ts +71 -0
  146. package/src/session/session-manager.ts +22 -186
  147. package/src/session/session-types.ts +312 -0
  148. package/src/session/stats.ts +387 -0
  149. package/src/session/streaming-edit.ts +258 -0
  150. package/src/session/ttsr.ts +213 -0
  151. package/src/slash-commands/builtin-registry.ts +0 -8
  152. package/src/stt/recorder.ts +2 -2
  153. package/src/system-prompt.ts +1 -14
  154. package/src/task/agents.ts +7 -33
  155. package/src/task/executor.ts +50 -438
  156. package/src/task/index.ts +104 -71
  157. package/src/task/progress-tracker.ts +390 -0
  158. package/src/task/render.ts +371 -187
  159. package/src/task/subprocess-tool-registry.ts +1 -1
  160. package/src/task/types.ts +14 -47
  161. package/src/tools/ask.ts +31 -42
  162. package/src/tools/bash-interactive.ts +2 -2
  163. package/src/tools/bash-interceptor.ts +2 -2
  164. package/src/tools/bash-normalize.ts +1 -1
  165. package/src/tools/bash-skill-urls.ts +2 -2
  166. package/src/tools/bash.ts +87 -136
  167. package/src/tools/browser.ts +54 -84
  168. package/src/tools/create-tools.ts +186 -0
  169. package/src/tools/default-renderer.ts +104 -0
  170. package/src/tools/explore.ts +11 -10
  171. package/src/tools/fetch.ts +24 -114
  172. package/src/tools/find.ts +48 -132
  173. package/src/tools/gemini-image.ts +5 -15
  174. package/src/tools/github.ts +450 -0
  175. package/src/tools/grep.ts +43 -179
  176. package/src/tools/index.ts +35 -198
  177. package/src/tools/json-tree.ts +3 -3
  178. package/src/tools/librarian.ts +18 -18
  179. package/src/tools/list-limit.ts +2 -2
  180. package/src/tools/notebook.ts +35 -87
  181. package/src/tools/oracle.ts +25 -25
  182. package/src/tools/output-meta.ts +89 -4
  183. package/src/tools/output-utils.ts +2 -2
  184. package/src/tools/python.ts +86 -637
  185. package/src/tools/read.ts +36 -119
  186. package/src/tools/reviewer-tool.ts +19 -21
  187. package/src/tools/search-code.ts +128 -0
  188. package/src/tools/ssh.ts +67 -126
  189. package/src/tools/subagent-tool.ts +197 -123
  190. package/src/tools/todo-write.ts +15 -31
  191. package/src/tools/tool-errors.ts +0 -30
  192. package/src/tools/undo-edit.ts +30 -67
  193. package/src/tools/write.ts +78 -127
  194. package/src/tui/code-cell.ts +4 -4
  195. package/src/tui/file-list.ts +2 -2
  196. package/src/tui/output-block.ts +1 -1
  197. package/src/tui/status-line.ts +1 -1
  198. package/src/tui/tree-list.ts +2 -2
  199. package/src/tui/types.ts +1 -1
  200. package/src/tui/utils.ts +1 -1
  201. package/src/{tools → ui}/render-utils.ts +87 -126
  202. package/src/utils/external-editor.ts +4 -4
  203. package/src/utils/file-mentions.ts +1 -1
  204. package/src/utils/index.ts +30 -0
  205. package/src/utils/tools-manager.ts +9 -19
  206. package/src/web/github-client.ts +290 -0
  207. package/src/web/scrapers/github.ts +11 -62
  208. package/src/web/search/auth.ts +1 -3
  209. package/src/web/search/index.ts +82 -46
  210. package/src/web/search/provider.ts +11 -16
  211. package/src/web/search/providers/grep.ts +160 -0
  212. package/src/web/search/render.ts +48 -235
  213. package/src/web/search/types.ts +1 -1
  214. package/src/commands/commit.ts +0 -36
  215. package/src/commit/agentic/agent.ts +0 -311
  216. package/src/commit/agentic/fallback.ts +0 -96
  217. package/src/commit/agentic/index.ts +0 -359
  218. package/src/commit/agentic/prompts/analyze-file.md +0 -22
  219. package/src/commit/agentic/prompts/session-user.md +0 -25
  220. package/src/commit/agentic/prompts/split-confirm.md +0 -1
  221. package/src/commit/agentic/prompts/system.md +0 -38
  222. package/src/commit/agentic/state.ts +0 -69
  223. package/src/commit/agentic/tools/analyze-file.ts +0 -118
  224. package/src/commit/agentic/tools/git-file-diff.ts +0 -194
  225. package/src/commit/agentic/tools/git-hunk.ts +0 -50
  226. package/src/commit/agentic/tools/git-overview.ts +0 -84
  227. package/src/commit/agentic/tools/index.ts +0 -56
  228. package/src/commit/agentic/tools/propose-changelog.ts +0 -128
  229. package/src/commit/agentic/tools/propose-commit.ts +0 -154
  230. package/src/commit/agentic/tools/recent-commits.ts +0 -81
  231. package/src/commit/agentic/tools/split-commit.ts +0 -280
  232. package/src/commit/agentic/topo-sort.ts +0 -44
  233. package/src/commit/agentic/trivial.ts +0 -51
  234. package/src/commit/agentic/validation.ts +0 -200
  235. package/src/commit/analysis/conventional.ts +0 -165
  236. package/src/commit/analysis/index.ts +0 -4
  237. package/src/commit/analysis/scope.ts +0 -242
  238. package/src/commit/analysis/summary.ts +0 -112
  239. package/src/commit/analysis/validation.ts +0 -66
  240. package/src/commit/changelog/detect.ts +0 -37
  241. package/src/commit/changelog/generate.ts +0 -110
  242. package/src/commit/changelog/index.ts +0 -234
  243. package/src/commit/changelog/parse.ts +0 -44
  244. package/src/commit/cli.ts +0 -93
  245. package/src/commit/git/diff.ts +0 -148
  246. package/src/commit/git/errors.ts +0 -9
  247. package/src/commit/git/index.ts +0 -211
  248. package/src/commit/git/operations.ts +0 -54
  249. package/src/commit/index.ts +0 -5
  250. package/src/commit/map-reduce/index.ts +0 -64
  251. package/src/commit/map-reduce/map-phase.ts +0 -178
  252. package/src/commit/map-reduce/reduce-phase.ts +0 -145
  253. package/src/commit/map-reduce/utils.ts +0 -9
  254. package/src/commit/message.ts +0 -11
  255. package/src/commit/model-selection.ts +0 -69
  256. package/src/commit/pipeline.ts +0 -243
  257. package/src/commit/prompts/analysis-system.md +0 -148
  258. package/src/commit/prompts/analysis-user.md +0 -38
  259. package/src/commit/prompts/changelog-system.md +0 -50
  260. package/src/commit/prompts/changelog-user.md +0 -18
  261. package/src/commit/prompts/file-observer-system.md +0 -24
  262. package/src/commit/prompts/file-observer-user.md +0 -8
  263. package/src/commit/prompts/reduce-system.md +0 -50
  264. package/src/commit/prompts/reduce-user.md +0 -17
  265. package/src/commit/prompts/summary-retry.md +0 -3
  266. package/src/commit/prompts/summary-system.md +0 -38
  267. package/src/commit/prompts/summary-user.md +0 -13
  268. package/src/commit/prompts/types-description.md +0 -2
  269. package/src/commit/types.ts +0 -109
  270. package/src/commit/utils/exclusions.ts +0 -42
  271. package/src/mcp/render.ts +0 -123
  272. package/src/modes/components/agent-dashboard.ts +0 -1130
  273. package/src/modes/components/codemode-group.ts +0 -369
  274. package/src/modes/components/read-tool-group.ts +0 -119
  275. package/src/modes/components/visual-truncate.ts +0 -63
  276. package/src/prompts/system/subagent-user-prompt.md +0 -8
  277. package/src/prompts/tools/ask.md +0 -44
  278. package/src/prompts/tools/bash.md +0 -24
  279. package/src/prompts/tools/browser.md +0 -33
  280. package/src/prompts/tools/calculator.md +0 -12
  281. package/src/prompts/tools/explore.md +0 -29
  282. package/src/prompts/tools/fetch.md +0 -16
  283. package/src/prompts/tools/find.md +0 -18
  284. package/src/prompts/tools/gemini-image.md +0 -23
  285. package/src/prompts/tools/grep.md +0 -28
  286. package/src/prompts/tools/hashline.md +0 -232
  287. package/src/prompts/tools/librarian.md +0 -24
  288. package/src/prompts/tools/lsp.md +0 -28
  289. package/src/prompts/tools/oracle.md +0 -26
  290. package/src/prompts/tools/patch.md +0 -74
  291. package/src/prompts/tools/python.md +0 -66
  292. package/src/prompts/tools/read.md +0 -36
  293. package/src/prompts/tools/replace.md +0 -38
  294. package/src/prompts/tools/reviewer.md +0 -41
  295. package/src/prompts/tools/ssh.md +0 -51
  296. package/src/prompts/tools/task-summary.md +0 -28
  297. package/src/prompts/tools/task.md +0 -146
  298. package/src/prompts/tools/todo-write.md +0 -65
  299. package/src/prompts/tools/undo-edit.md +0 -7
  300. package/src/prompts/tools/web-search.md +0 -19
  301. package/src/prompts/tools/write.md +0 -18
  302. package/src/task/batch.ts +0 -102
  303. package/src/task/discovery.ts +0 -126
  304. package/src/task/parallel.ts +0 -84
  305. package/src/task/template.ts +0 -32
  306. package/src/tools/calculator.ts +0 -537
  307. package/src/tools/jtd-to-typescript.ts +0 -198
  308. package/src/tools/renderers.ts +0 -60
  309. package/src/tools/tool-result.ts +0 -86
  310. /package/src/{modes/theme → theme}/dark.json +0 -0
  311. /package/src/{modes/theme → theme}/defaults/dark-catppuccin.json +0 -0
  312. /package/src/{modes/theme → theme}/defaults/dark-dracula.json +0 -0
  313. /package/src/{modes/theme → theme}/defaults/dark-gruvbox.json +0 -0
  314. /package/src/{modes/theme → theme}/defaults/dark-solarized.json +0 -0
  315. /package/src/{modes/theme → theme}/defaults/dark-tokyo-night.json +0 -0
  316. /package/src/{modes/theme → theme}/defaults/index.ts +0 -0
  317. /package/src/{modes/theme → theme}/defaults/light-catppuccin.json +0 -0
  318. /package/src/{modes/theme → theme}/defaults/light-github.json +0 -0
  319. /package/src/{modes/theme → theme}/defaults/light-solarized.json +0 -0
  320. /package/src/{modes/theme → theme}/light.json +0 -0
  321. /package/src/{modes/theme → theme}/mermaid-cache.ts +0 -0
  322. /package/src/{modes/theme → theme}/theme-schema.json +0 -0
  323. /package/src/{modes/theme → theme}/theme.ts +0 -0
@@ -0,0 +1,30 @@
1
+ export { EventBus } from "./event-bus";
2
+ export { getEditorCommand, type OpenInEditorOptions, openInEditor } from "./external-editor";
3
+ export {
4
+ type FileDisplayMode,
5
+ type FileDisplayModeSession,
6
+ resolveFileDisplayMode,
7
+ } from "./file-display-mode";
8
+ export { FrontmatterError, type FrontmatterOptions, parseFrontmatter } from "./frontmatter";
9
+ export { type FuzzyMatch, fuzzyFilter, fuzzyMatch } from "./fuzzy";
10
+ export {
11
+ addIgnoreRules,
12
+ addIgnoreRulesSync,
13
+ createIgnoreMatcher,
14
+ IGNORE_FILE_NAMES,
15
+ type IgnoreMatcher,
16
+ prefixIgnorePattern,
17
+ shouldIgnore,
18
+ toPosixPath,
19
+ } from "./ignore-files";
20
+ export { convertToPng } from "./image-convert";
21
+ export {
22
+ formatDimensionNote,
23
+ type ImageResizeOptions,
24
+ type ResizedImage,
25
+ resizeImage,
26
+ } from "./image-resize";
27
+ export { detectSupportedImageMimeTypeFromFile } from "./mime";
28
+ export { openPath } from "./open";
29
+ export { getOrCreateSnapshot, getSnapshotSourceCommand } from "./shell-snapshot";
30
+ export { printTimings, time } from "./timings";
@@ -2,7 +2,8 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { logger, ptree, TempDir } from "@nghyane/arcane-utils";
5
- import { APP_NAME, getToolsDir } from "@nghyane/arcane-utils/dirs";
5
+ import { getToolsDir } from "@nghyane/arcane-utils/dirs";
6
+ import { githubClient } from "../web/github-client";
6
7
 
7
8
  const TOOLS_DIR = getToolsDir();
8
9
  const TOOL_DOWNLOAD_TIMEOUT_MS = 15000;
@@ -120,25 +121,14 @@ export function getToolPath(tool: ToolName): string | null {
120
121
 
121
122
  // Fetch latest release version from GitHub
122
123
  async function getLatestVersion(repo: string, signal?: AbortSignal): Promise<string> {
123
- let response: Response;
124
- try {
125
- response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
126
- headers: { "User-Agent": `${APP_NAME}-coding-agent` },
127
- signal: ptree.combineSignals(signal, TOOL_METADATA_TIMEOUT_MS),
128
- });
129
- } catch (err) {
130
- if (err instanceof Error && err.name === "AbortError") {
131
- throw new Error("GitHub API request timed out");
132
- }
133
- throw err;
124
+ const result = await githubClient.request<{ tag_name: string }>(`/repos/${repo}/releases/latest`, {
125
+ timeout: Math.ceil(TOOL_METADATA_TIMEOUT_MS / 1000),
126
+ signal,
127
+ });
128
+ if (!result.ok) {
129
+ throw new Error(`GitHub API error: ${result.status}`);
134
130
  }
135
-
136
- if (!response.ok) {
137
- throw new Error(`GitHub API error: ${response.status}`);
138
- }
139
-
140
- const data = (await response.json()) as { tag_name: string };
141
- return data.tag_name.replace(/^v/, "");
131
+ return (result.data as { tag_name: string }).tag_name.replace(/^v/, "");
142
132
  }
143
133
 
144
134
  // Download a file from URL
@@ -0,0 +1,290 @@
1
+ import { $env, logger } from "@nghyane/arcane-utils";
2
+ import { $ } from "bun";
3
+
4
+ // =============================================================================
5
+ // Types
6
+ // =============================================================================
7
+
8
+ interface GitHubResponse<T = unknown> {
9
+ data: T;
10
+ ok: boolean;
11
+ status: number;
12
+ rateLimit?: RateLimitInfo;
13
+ }
14
+
15
+ interface RateLimitInfo {
16
+ remaining: number;
17
+ limit: number;
18
+ reset: number;
19
+ }
20
+
21
+ interface RequestOptions {
22
+ timeout?: number;
23
+ signal?: AbortSignal;
24
+ accept?: string;
25
+ mediaType?: string;
26
+ }
27
+
28
+ interface CacheEntry {
29
+ etag: string;
30
+ data: unknown;
31
+ timestamp: number;
32
+ }
33
+
34
+ // =============================================================================
35
+ // Auth
36
+ // =============================================================================
37
+
38
+ let cachedToken: string | undefined;
39
+ let tokenResolved = false;
40
+
41
+ async function resolveToken(): Promise<string | undefined> {
42
+ if (tokenResolved) return cachedToken;
43
+ tokenResolved = true;
44
+
45
+ // Env vars first
46
+ const envToken = $env.GITHUB_TOKEN || $env.GH_TOKEN;
47
+ if (envToken) {
48
+ cachedToken = envToken;
49
+ return cachedToken;
50
+ }
51
+
52
+ // Fallback: gh auth token
53
+ if (Bun.which("gh")) {
54
+ try {
55
+ const result = await $`gh auth token`.quiet().nothrow();
56
+ if (result.exitCode === 0) {
57
+ const token = result.text().trim();
58
+ if (token) {
59
+ cachedToken = token;
60
+ return cachedToken;
61
+ }
62
+ }
63
+ } catch {
64
+ // gh not authenticated
65
+ }
66
+ }
67
+
68
+ return undefined;
69
+ }
70
+
71
+ // =============================================================================
72
+ // ETag Cache
73
+ // =============================================================================
74
+
75
+ const etagCache = new Map<string, CacheEntry>();
76
+ const CACHE_MAX_SIZE = 200;
77
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
78
+
79
+ function getCacheKey(endpoint: string): string {
80
+ return endpoint;
81
+ }
82
+
83
+ function pruneCache(): void {
84
+ if (etagCache.size <= CACHE_MAX_SIZE) return;
85
+ const now = Date.now();
86
+ for (const [key, entry] of etagCache) {
87
+ if (now - entry.timestamp > CACHE_TTL_MS) {
88
+ etagCache.delete(key);
89
+ }
90
+ }
91
+ // If still too large, remove oldest
92
+ if (etagCache.size > CACHE_MAX_SIZE) {
93
+ const entries = [...etagCache.entries()].sort((a, b) => a[1].timestamp - b[1].timestamp);
94
+ const toRemove = entries.slice(0, etagCache.size - CACHE_MAX_SIZE);
95
+ for (const [key] of toRemove) {
96
+ etagCache.delete(key);
97
+ }
98
+ }
99
+ }
100
+
101
+ // =============================================================================
102
+ // Core Request
103
+ // =============================================================================
104
+
105
+ const RETRY_STATUS_CODES = new Set([429, 502, 503]);
106
+ const MAX_RETRIES = 3;
107
+
108
+ async function request<T = unknown>(endpoint: string, options: RequestOptions = {}): Promise<GitHubResponse<T>> {
109
+ const { timeout = 30, signal } = options;
110
+ const token = await resolveToken();
111
+
112
+ const headers: Record<string, string> = {
113
+ Accept: options.accept ?? "application/vnd.github.v3+json",
114
+ "User-Agent": "arcane-github-tool/1.0",
115
+ };
116
+
117
+ if (token) {
118
+ headers.Authorization = `Bearer ${token}`;
119
+ }
120
+
121
+ if (options.mediaType) {
122
+ headers.Accept = options.mediaType;
123
+ }
124
+
125
+ // ETag conditional request
126
+ const cacheKey = getCacheKey(endpoint);
127
+ const cached = etagCache.get(cacheKey);
128
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
129
+ headers["If-None-Match"] = cached.etag;
130
+ }
131
+
132
+ const url = endpoint.startsWith("https://") ? endpoint : `https://api.github.com${endpoint}`;
133
+
134
+ let lastError: Error | undefined;
135
+
136
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
137
+ try {
138
+ const controller = new AbortController();
139
+ const timeoutId = setTimeout(() => controller.abort(), timeout * 1000);
140
+
141
+ // Combine with external signal
142
+ if (signal?.aborted) {
143
+ clearTimeout(timeoutId);
144
+ return { data: null as T, ok: false, status: 0 };
145
+ }
146
+
147
+ const abortHandler = () => controller.abort();
148
+ signal?.addEventListener("abort", abortHandler, { once: true });
149
+
150
+ try {
151
+ const response = await fetch(url, {
152
+ headers,
153
+ signal: controller.signal,
154
+ });
155
+
156
+ clearTimeout(timeoutId);
157
+ signal?.removeEventListener("abort", abortHandler);
158
+
159
+ // Parse rate limit
160
+ const rateLimit: RateLimitInfo = {
161
+ remaining: parseInt(response.headers.get("x-ratelimit-remaining") ?? "-1", 10),
162
+ limit: parseInt(response.headers.get("x-ratelimit-limit") ?? "-1", 10),
163
+ reset: parseInt(response.headers.get("x-ratelimit-reset") ?? "0", 10),
164
+ };
165
+
166
+ if (rateLimit.remaining >= 0 && rateLimit.remaining < 10) {
167
+ logger.warn("GitHub API rate limit low", {
168
+ remaining: rateLimit.remaining,
169
+ reset: new Date(rateLimit.reset * 1000).toISOString(),
170
+ });
171
+ }
172
+
173
+ // 304 Not Modified — return cached data
174
+ if (response.status === 304 && cached) {
175
+ cached.timestamp = Date.now();
176
+ return { data: cached.data as T, ok: true, status: 304, rateLimit };
177
+ }
178
+
179
+ // Retry on transient errors
180
+ if (RETRY_STATUS_CODES.has(response.status) && attempt < MAX_RETRIES) {
181
+ const retryAfter = response.headers.get("retry-after");
182
+ const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : Math.min(1000 * 2 ** attempt, 10_000);
183
+
184
+ if (response.status === 429 && rateLimit.remaining === 0) {
185
+ const resetMs = rateLimit.reset * 1000 - Date.now();
186
+ if (resetMs > 30_000) {
187
+ return { data: null as T, ok: false, status: response.status, rateLimit };
188
+ }
189
+ await Bun.sleep(Math.max(resetMs, 1000));
190
+ } else {
191
+ await Bun.sleep(waitMs);
192
+ }
193
+ continue;
194
+ }
195
+
196
+ if (!response.ok) {
197
+ return { data: null as T, ok: false, status: response.status, rateLimit };
198
+ }
199
+
200
+ const data = (await response.json()) as T;
201
+
202
+ // Cache with ETag
203
+ const etag = response.headers.get("etag");
204
+ if (etag) {
205
+ etagCache.set(cacheKey, { etag, data, timestamp: Date.now() });
206
+ pruneCache();
207
+ }
208
+
209
+ return { data, ok: true, status: response.status, rateLimit };
210
+ } catch (err) {
211
+ clearTimeout(timeoutId);
212
+ signal?.removeEventListener("abort", abortHandler);
213
+ throw err;
214
+ }
215
+ } catch (err) {
216
+ lastError = err instanceof Error ? err : new Error(String(err));
217
+ if (attempt < MAX_RETRIES) {
218
+ await Bun.sleep(1000 * 2 ** attempt);
219
+ }
220
+ }
221
+ }
222
+
223
+ logger.error("GitHub API request failed", { endpoint, error: lastError?.message });
224
+ return { data: null as T, ok: false, status: 0 };
225
+ }
226
+
227
+ // =============================================================================
228
+ // Paginated Request
229
+ // =============================================================================
230
+
231
+ async function requestPaginated<T>(
232
+ endpoint: string,
233
+ options: RequestOptions & { perPage?: number; maxPages?: number } = {},
234
+ ): Promise<GitHubResponse<T[]>> {
235
+ const perPage = options.perPage ?? 30;
236
+ const maxPages = options.maxPages ?? 3;
237
+ const separator = endpoint.includes("?") ? "&" : "?";
238
+ const allData: T[] = [];
239
+
240
+ for (let page = 1; page <= maxPages; page++) {
241
+ const paginatedEndpoint = `${endpoint}${separator}per_page=${perPage}&page=${page}`;
242
+ const response = await request<T[]>(paginatedEndpoint, options);
243
+
244
+ if (!response.ok) {
245
+ if (allData.length > 0) {
246
+ return { data: allData, ok: true, status: response.status, rateLimit: response.rateLimit };
247
+ }
248
+ return response;
249
+ }
250
+
251
+ allData.push(...response.data);
252
+
253
+ // No more pages
254
+ if (response.data.length < perPage) break;
255
+ }
256
+
257
+ return { data: allData, ok: true, status: 200 };
258
+ }
259
+
260
+ // =============================================================================
261
+ // Raw content request (for file contents)
262
+ // =============================================================================
263
+
264
+ async function requestRaw(endpoint: string, options: RequestOptions = {}): Promise<GitHubResponse<string>> {
265
+ return request<string>(endpoint, {
266
+ ...options,
267
+ mediaType: "application/vnd.github.v3.raw",
268
+ });
269
+ }
270
+
271
+ // =============================================================================
272
+ // Public API
273
+ // =============================================================================
274
+
275
+ export const githubClient = {
276
+ request,
277
+ requestPaginated,
278
+ requestRaw,
279
+ resolveToken,
280
+
281
+ clearCache(): void {
282
+ etagCache.clear();
283
+ },
284
+
285
+ isAuthenticated(): Promise<boolean> {
286
+ return resolveToken().then(t => t !== undefined);
287
+ },
288
+ } as const;
289
+
290
+ export type { GitHubResponse, RateLimitInfo, RequestOptions };
@@ -1,4 +1,4 @@
1
- import { $env, ptree } from "@nghyane/arcane-utils";
1
+ import { githubClient } from "../github-client";
2
2
  import type { RenderResult, SpecialHandler } from "./types";
3
3
  import { finalizeOutput, loadPage } from "./types";
4
4
 
@@ -11,12 +11,6 @@ interface GitHubUrl {
11
11
  number?: number;
12
12
  }
13
13
 
14
- interface GitHubIssueComment {
15
- user: { login: string };
16
- created_at: string;
17
- body: string;
18
- }
19
-
20
14
  /**
21
15
  * Parse GitHub URL into components
22
16
  */
@@ -75,40 +69,15 @@ function toRawGitHubUrl(gh: GitHubUrl): string {
75
69
  }
76
70
 
77
71
  /**
78
- * Fetch from GitHub API
72
+ * Fetch from GitHub API — delegates to shared github-client.
79
73
  */
80
74
  export async function fetchGitHubApi(
81
75
  endpoint: string,
82
76
  timeout: number,
83
77
  signal?: AbortSignal,
84
78
  ): Promise<{ data: unknown; ok: boolean }> {
85
- try {
86
- const requestSignal = ptree.combineSignals(signal, timeout * 1000);
87
-
88
- const headers: Record<string, string> = {
89
- Accept: "application/vnd.github.v3+json",
90
- "User-Agent": "arc-web-fetch/1.0",
91
- };
92
-
93
- // Use GITHUB_TOKEN if available
94
- const token = $env.GITHUB_TOKEN || $env.GH_TOKEN;
95
- if (token) {
96
- headers.Authorization = `Bearer ${token}`;
97
- }
98
-
99
- const response = await fetch(`https://api.github.com${endpoint}`, {
100
- signal: requestSignal,
101
- headers,
102
- });
103
-
104
- if (!response.ok) {
105
- return { data: null, ok: false };
106
- }
107
-
108
- return { data: await response.json(), ok: true };
109
- } catch {
110
- return { data: null, ok: false };
111
- }
79
+ const result = await githubClient.request(endpoint, { timeout, signal });
80
+ return { data: result.data, ok: result.ok };
112
81
  }
113
82
 
114
83
  /**
@@ -118,35 +87,15 @@ async function fetchGitHubIssueComments(
118
87
  owner: string,
119
88
  repo: string,
120
89
  issueNumber: number,
121
- expectedCount: number,
90
+ _expectedCount: number,
122
91
  timeout: number,
123
92
  signal?: AbortSignal,
124
- ): Promise<GitHubIssueComment[]> {
125
- const perPage = 100;
126
- const comments: GitHubIssueComment[] = [];
127
-
128
- for (let page = 1; comments.length < expectedCount; page++) {
129
- const result = await fetchGitHubApi(
130
- `/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=${perPage}&page=${page}`,
131
- timeout,
132
- signal,
133
- );
134
- if (!result.ok || !Array.isArray(result.data)) {
135
- break;
136
- }
137
-
138
- const pageComments = result.data as GitHubIssueComment[];
139
- if (pageComments.length === 0) {
140
- break;
141
- }
142
-
143
- comments.push(...pageComments);
144
- if (pageComments.length < perPage) {
145
- break;
146
- }
147
- }
148
-
149
- return comments;
93
+ ): Promise<Array<{ user: { login: string }; created_at: string; body: string }>> {
94
+ const result = await githubClient.requestPaginated<{ user: { login: string }; created_at: string; body: string }>(
95
+ `/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
96
+ { timeout, signal, perPage: 100, maxPages: 5 },
97
+ );
98
+ return result.ok ? result.data : [];
150
99
  }
151
100
 
152
101
  /**
@@ -23,9 +23,7 @@ const DEFAULT_BASE_URL = "https://api.anthropic.com";
23
23
  */
24
24
  async function readJson<T>(filePath: string): Promise<T | null> {
25
25
  try {
26
- const file = Bun.file(filePath);
27
- if (!(await file.exists())) return null;
28
- const content = await file.text();
26
+ const content = await Bun.file(filePath).text();
29
27
  return JSON.parse(content) as T;
30
28
  } catch (error) {
31
29
  logger.warn("Failed to parse JSON file", { path: filePath, error: String(error) });