@nghyane/arcane 0.1.12 → 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 (333) 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/company.ts +2 -7
  30. package/src/exa/index.ts +1 -35
  31. package/src/exa/linkedin.ts +2 -7
  32. package/src/exa/mcp-client.ts +21 -11
  33. package/src/exa/render.ts +30 -190
  34. package/src/exa/researcher.ts +2 -12
  35. package/src/exa/search.ts +5 -25
  36. package/src/exa/types.ts +3 -3
  37. package/src/exec/bash-executor.ts +2 -1
  38. package/src/exec/non-interactive-env.ts +43 -0
  39. package/src/export/html/index.ts +1 -1
  40. package/src/extensibility/custom-tools/loader.ts +1 -1
  41. package/src/extensibility/custom-tools/types.ts +5 -1
  42. package/src/extensibility/custom-tools/wrapper.ts +1 -1
  43. package/src/extensibility/extensions/runner.ts +1 -1
  44. package/src/extensibility/extensions/types.ts +1 -1
  45. package/src/extensibility/extensions/wrapper.ts +7 -15
  46. package/src/extensibility/hooks/runner.ts +1 -1
  47. package/src/extensibility/hooks/types.ts +1 -1
  48. package/src/extensibility/plugins/doctor.ts +1 -1
  49. package/src/index.ts +13 -13
  50. package/src/lsp/index.ts +77 -24
  51. package/src/lsp/render.ts +34 -583
  52. package/src/lsp/types.ts +3 -3
  53. package/src/lsp/utils.ts +1 -1
  54. package/src/main.ts +1 -1
  55. package/src/mcp/tool-bridge.ts +1 -24
  56. package/src/modes/components/assistant-message.ts +7 -7
  57. package/src/modes/components/bash-execution.ts +48 -113
  58. package/src/modes/components/bordered-loader.ts +1 -1
  59. package/src/modes/components/branch-summary-message.ts +13 -10
  60. package/src/modes/components/compaction-summary-message.ts +14 -13
  61. package/src/modes/components/context-group.ts +106 -0
  62. package/src/modes/components/custom-message.ts +4 -5
  63. package/src/modes/components/diff.ts +2 -2
  64. package/src/modes/components/dynamic-border.ts +1 -1
  65. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  66. package/src/modes/components/extensions/extension-list.ts +1 -1
  67. package/src/modes/components/extensions/inspector-panel.ts +8 -3
  68. package/src/modes/components/footer.ts +2 -2
  69. package/src/modes/components/history-search.ts +1 -1
  70. package/src/modes/components/hook-editor.ts +1 -1
  71. package/src/modes/components/hook-input.ts +1 -1
  72. package/src/modes/components/hook-message.ts +4 -5
  73. package/src/modes/components/hook-selector.ts +1 -1
  74. package/src/modes/components/index.ts +0 -2
  75. package/src/modes/components/keybinding-hints.ts +1 -1
  76. package/src/modes/components/login-dialog.ts +1 -1
  77. package/src/modes/components/mcp-add-wizard.ts +1 -1
  78. package/src/modes/components/model-selector.ts +1 -1
  79. package/src/modes/components/oauth-selector.ts +1 -1
  80. package/src/modes/components/plugin-settings.ts +1 -1
  81. package/src/modes/components/python-execution.ts +49 -92
  82. package/src/modes/components/queue-mode-selector.ts +1 -1
  83. package/src/modes/components/session-selector.ts +1 -1
  84. package/src/modes/components/settings-defs.ts +5 -10
  85. package/src/modes/components/settings-selector.ts +1 -1
  86. package/src/modes/components/show-images-selector.ts +1 -1
  87. package/src/modes/components/skill-message.ts +4 -4
  88. package/src/modes/components/status-line/segments.ts +2 -2
  89. package/src/modes/components/status-line/separators.ts +1 -1
  90. package/src/modes/components/status-line-segment-editor.ts +1 -1
  91. package/src/modes/components/status-line.ts +1 -1
  92. package/src/modes/components/theme-selector.ts +1 -1
  93. package/src/modes/components/thinking-selector.ts +1 -1
  94. package/src/modes/components/todo-display.ts +2 -4
  95. package/src/modes/components/todo-reminder.ts +4 -4
  96. package/src/modes/components/tool-execution.ts +118 -440
  97. package/src/modes/components/tool-image-display.ts +107 -0
  98. package/src/modes/components/tree-selector.ts +2 -2
  99. package/src/modes/components/ttsr-notification.ts +4 -17
  100. package/src/modes/components/user-message-selector.ts +1 -1
  101. package/src/modes/components/user-message.ts +9 -10
  102. package/src/modes/components/welcome.ts +1 -1
  103. package/src/modes/controllers/command-controller.ts +1 -1
  104. package/src/modes/controllers/event-controller.ts +58 -187
  105. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  106. package/src/modes/controllers/input-controller.ts +3 -1
  107. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  108. package/src/modes/controllers/selector-controller.ts +3 -26
  109. package/src/modes/controllers/ssh-command-controller.ts +1 -1
  110. package/src/modes/interactive-mode.ts +3 -7
  111. package/src/modes/print-mode.ts +5 -5
  112. package/src/modes/rpc/rpc-mode.ts +1 -1
  113. package/src/modes/types.ts +1 -2
  114. package/src/modes/utils/ui-helpers.ts +34 -32
  115. package/src/patch/edit-tool.ts +742 -0
  116. package/src/patch/index.ts +32 -898
  117. package/src/patch/schemas.ts +208 -0
  118. package/src/patch/shared.ts +83 -151
  119. package/src/prompts/agents/explore.md +22 -37
  120. package/src/prompts/agents/frontmatter.md +1 -1
  121. package/src/prompts/agents/init.md +2 -2
  122. package/src/prompts/agents/librarian.md +30 -21
  123. package/src/prompts/agents/oracle.md +9 -2
  124. package/src/prompts/agents/reviewer.md +15 -49
  125. package/src/prompts/agents/task.md +17 -9
  126. package/src/prompts/compaction/branch-summary-context.md +1 -1
  127. package/src/prompts/compaction/branch-summary-preamble.md +1 -1
  128. package/src/prompts/compaction/branch-summary.md +4 -1
  129. package/src/prompts/compaction/compaction-short-summary.md +1 -1
  130. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  131. package/src/prompts/compaction/compaction-summary.md +4 -1
  132. package/src/prompts/compaction/compaction-turn-prefix.md +1 -1
  133. package/src/prompts/compaction/compaction-update-summary.md +1 -1
  134. package/src/prompts/memories/consolidation.md +1 -1
  135. package/src/prompts/memories/read_path.md +1 -1
  136. package/src/prompts/memories/stage_one_input.md +1 -1
  137. package/src/prompts/memories/stage_one_system.md +1 -1
  138. package/src/prompts/review-request.md +1 -1
  139. package/src/prompts/system/agent-creation-architect.md +1 -1
  140. package/src/prompts/system/agent-creation-user.md +1 -1
  141. package/src/prompts/system/custom-system-prompt.md +1 -1
  142. package/src/prompts/system/file-operations.md +1 -1
  143. package/src/prompts/system/subagent-system-prompt.md +2 -2
  144. package/src/prompts/system/summarization-system.md +1 -1
  145. package/src/prompts/system/system-prompt.md +163 -178
  146. package/src/prompts/system/title-system.md +1 -1
  147. package/src/prompts/system/ttsr-interrupt.md +1 -1
  148. package/src/prompts/system/verification-reminder.md +6 -0
  149. package/src/prompts/system/web-search.md +1 -1
  150. package/src/sdk.ts +0 -9
  151. package/src/session/agent-session.ts +244 -1459
  152. package/src/session/auth-storage.ts +5 -0
  153. package/src/session/model-controller.ts +406 -0
  154. package/src/session/retry-utils.ts +71 -0
  155. package/src/session/session-manager.ts +22 -186
  156. package/src/session/session-types.ts +312 -0
  157. package/src/session/stats.ts +387 -0
  158. package/src/session/streaming-edit.ts +258 -0
  159. package/src/session/ttsr.ts +213 -0
  160. package/src/slash-commands/builtin-registry.ts +0 -8
  161. package/src/ssh/connection-manager.ts +1 -0
  162. package/src/stt/recorder.ts +2 -2
  163. package/src/system-prompt.ts +1 -14
  164. package/src/task/agents.ts +7 -33
  165. package/src/task/executor.ts +50 -438
  166. package/src/task/index.ts +104 -71
  167. package/src/task/progress-tracker.ts +390 -0
  168. package/src/task/render.ts +371 -187
  169. package/src/task/subprocess-tool-registry.ts +1 -1
  170. package/src/task/types.ts +14 -47
  171. package/src/tools/ask.ts +31 -42
  172. package/src/tools/bash-interactive.ts +4 -47
  173. package/src/tools/bash-interceptor.ts +2 -2
  174. package/src/tools/bash-normalize.ts +1 -1
  175. package/src/tools/bash-skill-urls.ts +2 -2
  176. package/src/tools/bash.ts +87 -136
  177. package/src/tools/browser.ts +54 -84
  178. package/src/tools/create-tools.ts +186 -0
  179. package/src/tools/default-renderer.ts +104 -0
  180. package/src/tools/explore.ts +11 -10
  181. package/src/tools/fetch.ts +24 -114
  182. package/src/tools/find.ts +48 -132
  183. package/src/tools/gemini-image.ts +5 -15
  184. package/src/tools/github.ts +450 -0
  185. package/src/tools/grep.ts +43 -179
  186. package/src/tools/index.ts +35 -198
  187. package/src/tools/json-tree.ts +3 -3
  188. package/src/tools/librarian.ts +18 -18
  189. package/src/tools/list-limit.ts +2 -2
  190. package/src/tools/notebook.ts +35 -87
  191. package/src/tools/oracle.ts +25 -25
  192. package/src/tools/output-meta.ts +89 -4
  193. package/src/tools/output-utils.ts +2 -2
  194. package/src/tools/python.ts +86 -637
  195. package/src/tools/read.ts +36 -119
  196. package/src/tools/reviewer-tool.ts +19 -21
  197. package/src/tools/search-code.ts +128 -0
  198. package/src/tools/ssh.ts +67 -126
  199. package/src/tools/subagent-tool.ts +197 -123
  200. package/src/tools/todo-write.ts +15 -31
  201. package/src/tools/tool-errors.ts +0 -30
  202. package/src/tools/undo-edit.ts +30 -67
  203. package/src/tools/write.ts +78 -127
  204. package/src/tui/code-cell.ts +4 -4
  205. package/src/tui/file-list.ts +2 -2
  206. package/src/tui/output-block.ts +1 -1
  207. package/src/tui/status-line.ts +1 -1
  208. package/src/tui/tree-list.ts +2 -2
  209. package/src/tui/types.ts +1 -1
  210. package/src/tui/utils.ts +1 -1
  211. package/src/{tools → ui}/render-utils.ts +87 -126
  212. package/src/utils/external-editor.ts +4 -4
  213. package/src/utils/file-mentions.ts +1 -1
  214. package/src/utils/index.ts +30 -0
  215. package/src/utils/tools-manager.ts +9 -19
  216. package/src/web/github-client.ts +290 -0
  217. package/src/web/scrapers/github.ts +11 -62
  218. package/src/web/search/auth.ts +1 -3
  219. package/src/web/search/index.ts +85 -49
  220. package/src/web/search/provider.ts +11 -16
  221. package/src/web/search/providers/grep.ts +160 -0
  222. package/src/web/search/render.ts +48 -235
  223. package/src/web/search/types.ts +1 -1
  224. package/src/commands/commit.ts +0 -36
  225. package/src/commit/agentic/agent.ts +0 -311
  226. package/src/commit/agentic/fallback.ts +0 -96
  227. package/src/commit/agentic/index.ts +0 -359
  228. package/src/commit/agentic/prompts/analyze-file.md +0 -22
  229. package/src/commit/agentic/prompts/session-user.md +0 -25
  230. package/src/commit/agentic/prompts/split-confirm.md +0 -1
  231. package/src/commit/agentic/prompts/system.md +0 -38
  232. package/src/commit/agentic/state.ts +0 -69
  233. package/src/commit/agentic/tools/analyze-file.ts +0 -118
  234. package/src/commit/agentic/tools/git-file-diff.ts +0 -194
  235. package/src/commit/agentic/tools/git-hunk.ts +0 -50
  236. package/src/commit/agentic/tools/git-overview.ts +0 -84
  237. package/src/commit/agentic/tools/index.ts +0 -56
  238. package/src/commit/agentic/tools/propose-changelog.ts +0 -128
  239. package/src/commit/agentic/tools/propose-commit.ts +0 -154
  240. package/src/commit/agentic/tools/recent-commits.ts +0 -81
  241. package/src/commit/agentic/tools/split-commit.ts +0 -280
  242. package/src/commit/agentic/topo-sort.ts +0 -44
  243. package/src/commit/agentic/trivial.ts +0 -51
  244. package/src/commit/agentic/validation.ts +0 -200
  245. package/src/commit/analysis/conventional.ts +0 -165
  246. package/src/commit/analysis/index.ts +0 -4
  247. package/src/commit/analysis/scope.ts +0 -242
  248. package/src/commit/analysis/summary.ts +0 -112
  249. package/src/commit/analysis/validation.ts +0 -66
  250. package/src/commit/changelog/detect.ts +0 -37
  251. package/src/commit/changelog/generate.ts +0 -110
  252. package/src/commit/changelog/index.ts +0 -234
  253. package/src/commit/changelog/parse.ts +0 -44
  254. package/src/commit/cli.ts +0 -93
  255. package/src/commit/git/diff.ts +0 -148
  256. package/src/commit/git/errors.ts +0 -9
  257. package/src/commit/git/index.ts +0 -211
  258. package/src/commit/git/operations.ts +0 -54
  259. package/src/commit/index.ts +0 -5
  260. package/src/commit/map-reduce/index.ts +0 -64
  261. package/src/commit/map-reduce/map-phase.ts +0 -178
  262. package/src/commit/map-reduce/reduce-phase.ts +0 -145
  263. package/src/commit/map-reduce/utils.ts +0 -9
  264. package/src/commit/message.ts +0 -11
  265. package/src/commit/model-selection.ts +0 -69
  266. package/src/commit/pipeline.ts +0 -243
  267. package/src/commit/prompts/analysis-system.md +0 -148
  268. package/src/commit/prompts/analysis-user.md +0 -38
  269. package/src/commit/prompts/changelog-system.md +0 -50
  270. package/src/commit/prompts/changelog-user.md +0 -18
  271. package/src/commit/prompts/file-observer-system.md +0 -24
  272. package/src/commit/prompts/file-observer-user.md +0 -8
  273. package/src/commit/prompts/reduce-system.md +0 -50
  274. package/src/commit/prompts/reduce-user.md +0 -17
  275. package/src/commit/prompts/summary-retry.md +0 -3
  276. package/src/commit/prompts/summary-system.md +0 -38
  277. package/src/commit/prompts/summary-user.md +0 -13
  278. package/src/commit/prompts/types-description.md +0 -2
  279. package/src/commit/types.ts +0 -109
  280. package/src/commit/utils/exclusions.ts +0 -42
  281. package/src/mcp/render.ts +0 -123
  282. package/src/modes/components/agent-dashboard.ts +0 -1130
  283. package/src/modes/components/codemode-group.ts +0 -369
  284. package/src/modes/components/read-tool-group.ts +0 -119
  285. package/src/modes/components/visual-truncate.ts +0 -63
  286. package/src/prompts/system/subagent-user-prompt.md +0 -8
  287. package/src/prompts/tools/ask.md +0 -44
  288. package/src/prompts/tools/bash.md +0 -24
  289. package/src/prompts/tools/browser.md +0 -33
  290. package/src/prompts/tools/calculator.md +0 -12
  291. package/src/prompts/tools/explore.md +0 -29
  292. package/src/prompts/tools/fetch.md +0 -16
  293. package/src/prompts/tools/find.md +0 -18
  294. package/src/prompts/tools/gemini-image.md +0 -23
  295. package/src/prompts/tools/grep.md +0 -28
  296. package/src/prompts/tools/hashline.md +0 -232
  297. package/src/prompts/tools/librarian.md +0 -24
  298. package/src/prompts/tools/lsp.md +0 -28
  299. package/src/prompts/tools/oracle.md +0 -26
  300. package/src/prompts/tools/patch.md +0 -74
  301. package/src/prompts/tools/python.md +0 -66
  302. package/src/prompts/tools/read.md +0 -36
  303. package/src/prompts/tools/replace.md +0 -38
  304. package/src/prompts/tools/reviewer.md +0 -41
  305. package/src/prompts/tools/ssh.md +0 -51
  306. package/src/prompts/tools/task-summary.md +0 -28
  307. package/src/prompts/tools/task.md +0 -146
  308. package/src/prompts/tools/todo-write.md +0 -65
  309. package/src/prompts/tools/undo-edit.md +0 -7
  310. package/src/prompts/tools/web-search.md +0 -19
  311. package/src/prompts/tools/write.md +0 -18
  312. package/src/task/batch.ts +0 -102
  313. package/src/task/discovery.ts +0 -126
  314. package/src/task/parallel.ts +0 -84
  315. package/src/task/template.ts +0 -32
  316. package/src/tools/calculator.ts +0 -537
  317. package/src/tools/jtd-to-typescript.ts +0 -198
  318. package/src/tools/renderers.ts +0 -60
  319. package/src/tools/tool-result.ts +0 -86
  320. /package/src/{modes/theme → theme}/dark.json +0 -0
  321. /package/src/{modes/theme → theme}/defaults/dark-catppuccin.json +0 -0
  322. /package/src/{modes/theme → theme}/defaults/dark-dracula.json +0 -0
  323. /package/src/{modes/theme → theme}/defaults/dark-gruvbox.json +0 -0
  324. /package/src/{modes/theme → theme}/defaults/dark-solarized.json +0 -0
  325. /package/src/{modes/theme → theme}/defaults/dark-tokyo-night.json +0 -0
  326. /package/src/{modes/theme → theme}/defaults/index.ts +0 -0
  327. /package/src/{modes/theme → theme}/defaults/light-catppuccin.json +0 -0
  328. /package/src/{modes/theme → theme}/defaults/light-github.json +0 -0
  329. /package/src/{modes/theme → theme}/defaults/light-solarized.json +0 -0
  330. /package/src/{modes/theme → theme}/light.json +0 -0
  331. /package/src/{modes/theme → theme}/mermaid-cache.ts +0 -0
  332. /package/src/{modes/theme → theme}/theme-schema.json +0 -0
  333. /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) });