@co0ontty/wand 4.57.0 → 4.58.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 (177) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/ensure-node-pty-helper.d.ts +0 -1
  3. package/dist/ensure-node-pty-helper.js +0 -5
  4. package/dist/git-quick-commit.js +18 -19
  5. package/dist/github-connector.d.ts +11 -5
  6. package/dist/github-connector.js +105 -33
  7. package/dist/missions.d.ts +1 -1
  8. package/dist/missions.js +17 -4
  9. package/dist/server-file-routes.js +43 -2
  10. package/dist/server-github-routes.js +50 -53
  11. package/dist/server-mission-routes.js +11 -10
  12. package/dist/server-task-routes.d.ts +14 -2
  13. package/dist/server-task-routes.js +248 -84
  14. package/dist/server-workspace-routes.js +90 -16
  15. package/dist/server.js +2 -20
  16. package/dist/storage.d.ts +23 -6
  17. package/dist/storage.js +130 -53
  18. package/dist/structured-exec-host.d.ts +13 -2
  19. package/dist/structured-exec-host.js +71 -36
  20. package/dist/structured-exec-pump.d.ts +1 -1
  21. package/dist/structured-exec-pump.js +14 -8
  22. package/dist/task-types.d.ts +15 -0
  23. package/dist/terminal-daemon-client.d.ts +2 -0
  24. package/dist/terminal-daemon-client.js +7 -1
  25. package/dist/terminal-daemon-server.js +17 -18
  26. package/dist/tui/ipc-server.js +3 -1
  27. package/dist/types.d.ts +1 -0
  28. package/dist/wand-task-sync.d.ts +9 -0
  29. package/dist/wand-task-sync.js +50 -0
  30. package/dist/web-ui/content/scripts.js +85 -85
  31. package/dist/web-ui/content/styles.css +1 -1
  32. package/dist/web-ui/embedded-assets.js +2 -2
  33. package/dist/ws-broadcast.js +5 -1
  34. package/package.json +2 -3
  35. package/dist/taskboard-bridge.d.ts +0 -11
  36. package/dist/taskboard-bridge.js +0 -371
  37. package/vendor/codex-taskboard/LICENSE +0 -201
  38. package/vendor/codex-taskboard/dist/web/assets/AiChat-BNn4VJ5a.js +0 -12
  39. package/vendor/codex-taskboard/dist/web/assets/GanttView-D8wnfGrG.css +0 -1
  40. package/vendor/codex-taskboard/dist/web/assets/GanttView-rGWhuEX9.js +0 -39
  41. package/vendor/codex-taskboard/dist/web/assets/abnfDiagram-N423BO3Z-DbFG6lbA.js +0 -1
  42. package/vendor/codex-taskboard/dist/web/assets/arc-BjSQqbzd.js +0 -1
  43. package/vendor/codex-taskboard/dist/web/assets/architecture-TIHT7OUA-C9-t8p4o.js +0 -1
  44. package/vendor/codex-taskboard/dist/web/assets/architectureDiagram-T3A2C74G-Db4kJZwO.js +0 -36
  45. package/vendor/codex-taskboard/dist/web/assets/array-BifhSqXX.js +0 -1
  46. package/vendor/codex-taskboard/dist/web/assets/blockDiagram-VBNYF7ZC-C-BywuWV.js +0 -132
  47. package/vendor/codex-taskboard/dist/web/assets/c4Diagram-5PPSVZJV-Dq-80rjP.js +0 -10
  48. package/vendor/codex-taskboard/dist/web/assets/channel-C7mnz8Xc.js +0 -1
  49. package/vendor/codex-taskboard/dist/web/assets/chunk-2GRJ4B5K-B2G57us9.js +0 -1
  50. package/vendor/codex-taskboard/dist/web/assets/chunk-2Q5K7J3B-C1jixKkw.js +0 -1
  51. package/vendor/codex-taskboard/dist/web/assets/chunk-3NCLNEKW-DVGIRib_.js +0 -1
  52. package/vendor/codex-taskboard/dist/web/assets/chunk-4I5QYGJK-kNoGFqr0.js +0 -1
  53. package/vendor/codex-taskboard/dist/web/assets/chunk-5RXB4S5H-4iv6-xM6.js +0 -231
  54. package/vendor/codex-taskboard/dist/web/assets/chunk-5VM5RSS4-ZNzvKenW.js +0 -15
  55. package/vendor/codex-taskboard/dist/web/assets/chunk-6Q2QTUOP-1J34vxHr.js +0 -88
  56. package/vendor/codex-taskboard/dist/web/assets/chunk-7BUUIJ7U-Bb538aSH.js +0 -1
  57. package/vendor/codex-taskboard/dist/web/assets/chunk-7Z6QIM7H-CDQpQlxX.js +0 -10
  58. package/vendor/codex-taskboard/dist/web/assets/chunk-GF5L2VYU-Dg92oHHl.js +0 -206
  59. package/vendor/codex-taskboard/dist/web/assets/chunk-I66GZJ75-Bk8_gFs2.js +0 -125
  60. package/vendor/codex-taskboard/dist/web/assets/chunk-J7OUQ5F2-CJRcenlr.js +0 -2
  61. package/vendor/codex-taskboard/dist/web/assets/chunk-JQJVKLGR-BLBV_Sus.js +0 -156
  62. package/vendor/codex-taskboard/dist/web/assets/chunk-JWPE2WC7-DVXcaiue.js +0 -1
  63. package/vendor/codex-taskboard/dist/web/assets/chunk-KBJHAD2P-Da9Yjz0p.js +0 -1
  64. package/vendor/codex-taskboard/dist/web/assets/chunk-KEIR6QF5-Dj-OpFgW.js +0 -161
  65. package/vendor/codex-taskboard/dist/web/assets/chunk-NSK5VX7P-Cb56tnGK.js +0 -2
  66. package/vendor/codex-taskboard/dist/web/assets/chunk-QR6OTTB3-BwoszTxn.js +0 -62
  67. package/vendor/codex-taskboard/dist/web/assets/chunk-RYQCIY6F-D_L2RdcQ.js +0 -1
  68. package/vendor/codex-taskboard/dist/web/assets/chunk-UBXNYLIW-YX5mN8cl.js +0 -1
  69. package/vendor/codex-taskboard/dist/web/assets/chunk-W5SLKNZC-DlOLhhDO.js +0 -1
  70. package/vendor/codex-taskboard/dist/web/assets/chunk-WRU74C26-B7SBmP0K.js +0 -70
  71. package/vendor/codex-taskboard/dist/web/assets/chunk-XXDRQBXY-BuE3VzE_.js +0 -1
  72. package/vendor/codex-taskboard/dist/web/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +0 -1
  73. package/vendor/codex-taskboard/dist/web/assets/chunk-ZIRB5QZD-C6fEPe3t.js +0 -32
  74. package/vendor/codex-taskboard/dist/web/assets/classDiagram-JCYQIIEL-KIdCVvbA.js +0 -1
  75. package/vendor/codex-taskboard/dist/web/assets/classDiagram-v2-OCEON4UE-KIdCVvbA.js +0 -1
  76. package/vendor/codex-taskboard/dist/web/assets/cose-bilkent-JH36ORCC-BzQvy5TC.js +0 -1
  77. package/vendor/codex-taskboard/dist/web/assets/cynefin-VYW2F7L2-D1_5O1XR.js +0 -1
  78. package/vendor/codex-taskboard/dist/web/assets/cynefinDiagram-MW4NZA55-DOW_U5QN.js +0 -62
  79. package/vendor/codex-taskboard/dist/web/assets/cytoscape.esm-DnGd5Fuo.js +0 -321
  80. package/vendor/codex-taskboard/dist/web/assets/dagre-VZM6K2ZE-0Dntc3NT.js +0 -4
  81. package/vendor/codex-taskboard/dist/web/assets/dagre-dpRSp0QF.js +0 -1
  82. package/vendor/codex-taskboard/dist/web/assets/defaultLocale-C8Fc0cco.js +0 -1
  83. package/vendor/codex-taskboard/dist/web/assets/diagram-7IWD3JNH-D7GleYAM.js +0 -30
  84. package/vendor/codex-taskboard/dist/web/assets/diagram-B4RE2ZJO-DCXTe6q5.js +0 -3
  85. package/vendor/codex-taskboard/dist/web/assets/diagram-LBJQPF4R-ZFJyXEXl.js +0 -24
  86. package/vendor/codex-taskboard/dist/web/assets/diagram-Q27KOJAE-DUGCuGd9.js +0 -24
  87. package/vendor/codex-taskboard/dist/web/assets/diagram-UB23O5K3-DRmp4Nng.js +0 -41
  88. package/vendor/codex-taskboard/dist/web/assets/dist-D9sYb5Oa.js +0 -1
  89. package/vendor/codex-taskboard/dist/web/assets/ebnfDiagram-BXEA7PRR-CkKoaf7T.js +0 -1
  90. package/vendor/codex-taskboard/dist/web/assets/erDiagram-JOGREHBK-nBX5z4R4.js +0 -85
  91. package/vendor/codex-taskboard/dist/web/assets/eventmodeling-45OFAUF4-pexmK65N.js +0 -1
  92. package/vendor/codex-taskboard/dist/web/assets/flowDiagram-UKHOOZJN-aroohRbL.js +0 -1
  93. package/vendor/codex-taskboard/dist/web/assets/ganttDiagram-PKOTCBZU-BOJWUTXa.js +0 -292
  94. package/vendor/codex-taskboard/dist/web/assets/gitGraph-TEB2WS4Q-DxnuhSZU.js +0 -1
  95. package/vendor/codex-taskboard/dist/web/assets/gitGraphDiagram-DS77QQ5N-Cte975TC.js +0 -106
  96. package/vendor/codex-taskboard/dist/web/assets/graphlib-DS17s2tU.js +0 -1
  97. package/vendor/codex-taskboard/dist/web/assets/index-Docmx4uQ.css +0 -1
  98. package/vendor/codex-taskboard/dist/web/assets/index-Du07lEGz.js +0 -121
  99. package/vendor/codex-taskboard/dist/web/assets/info-DKCQHKI2-DOTjgYxk.js +0 -1
  100. package/vendor/codex-taskboard/dist/web/assets/infoDiagram-6WML65LV-SpyV9UJd.js +0 -2
  101. package/vendor/codex-taskboard/dist/web/assets/init-D6jRqBbL.js +0 -1
  102. package/vendor/codex-taskboard/dist/web/assets/ishikawaDiagram-WSZJBQD7-T0PZwoUZ.js +0 -70
  103. package/vendor/codex-taskboard/dist/web/assets/journeyDiagram-NVQOT4AX-B88b8UgX.js +0 -139
  104. package/vendor/codex-taskboard/dist/web/assets/kanban-definition-27J2QSJJ-CIlfNnl_.js +0 -89
  105. package/vendor/codex-taskboard/dist/web/assets/katex-B7rAX3Vi.js +0 -257
  106. package/vendor/codex-taskboard/dist/web/assets/line-fWrwPZhB.js +0 -1
  107. package/vendor/codex-taskboard/dist/web/assets/linear-B7l8qgEw.js +0 -1
  108. package/vendor/codex-taskboard/dist/web/assets/map-BaFkSB1l.js +0 -1
  109. package/vendor/codex-taskboard/dist/web/assets/mermaid-parser.core-C5gls2nC.js +0 -7
  110. package/vendor/codex-taskboard/dist/web/assets/mermaid.core-POz2Xcvr.js +0 -11
  111. package/vendor/codex-taskboard/dist/web/assets/mindmap-definition-FAOFIHXS-BM0-egZ5.js +0 -96
  112. package/vendor/codex-taskboard/dist/web/assets/ordinal-hYBb2elL.js +0 -1
  113. package/vendor/codex-taskboard/dist/web/assets/packet-7NZHBO7P-DQZzyVos.js +0 -1
  114. package/vendor/codex-taskboard/dist/web/assets/path-BWPyau1x.js +0 -1
  115. package/vendor/codex-taskboard/dist/web/assets/pegDiagram-VL7TDLO6-B8aEwl-7.js +0 -1
  116. package/vendor/codex-taskboard/dist/web/assets/pie-RZYD4A2V-DRUflilR.js +0 -1
  117. package/vendor/codex-taskboard/dist/web/assets/pieDiagram-7S7Q4E2Y-0cDDxlua.js +0 -39
  118. package/vendor/codex-taskboard/dist/web/assets/purify.es-Bwt4ulIK.js +0 -3
  119. package/vendor/codex-taskboard/dist/web/assets/quadrantDiagram-CIZ2JOQS-CBgb0YZ0.js +0 -7
  120. package/vendor/codex-taskboard/dist/web/assets/radar-I7S5WNFK-tRocoKxq.js +0 -1
  121. package/vendor/codex-taskboard/dist/web/assets/railroad-3IZDKUUU-DaS4ZlX_.js +0 -1
  122. package/vendor/codex-taskboard/dist/web/assets/railroad-abnf-AHOZXSZD-BN1T5dBT.js +0 -1
  123. package/vendor/codex-taskboard/dist/web/assets/railroad-ebnf-EBAXGLYW-Cuf8qyKs.js +0 -1
  124. package/vendor/codex-taskboard/dist/web/assets/railroad-peg-LSFZ7HO6-B511_EYq.js +0 -1
  125. package/vendor/codex-taskboard/dist/web/assets/railroadDiagram-AXF67PYL-QtPo-JZb.js +0 -1
  126. package/vendor/codex-taskboard/dist/web/assets/requirementDiagram-LRYGKXZP-CuBIF85r.js +0 -84
  127. package/vendor/codex-taskboard/dist/web/assets/rolldown-runtime-QTnfLwEv.js +0 -1
  128. package/vendor/codex-taskboard/dist/web/assets/rough.esm-CSKSodPl.js +0 -1
  129. package/vendor/codex-taskboard/dist/web/assets/sankeyDiagram-W5VNT64P-CRA-b_za.js +0 -40
  130. package/vendor/codex-taskboard/dist/web/assets/sequenceDiagram-SI44F4Z6-CoddVWwY.js +0 -162
  131. package/vendor/codex-taskboard/dist/web/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +0 -1
  132. package/vendor/codex-taskboard/dist/web/assets/src-_wZywoZs.js +0 -1
  133. package/vendor/codex-taskboard/dist/web/assets/stateDiagram-OKZ733FA-D8DMM2ln.js +0 -1
  134. package/vendor/codex-taskboard/dist/web/assets/stateDiagram-v2-UEYNNEHI-CVp0eYAS.js +0 -1
  135. package/vendor/codex-taskboard/dist/web/assets/swimlanes-SLNWSIFB-C1fC7EFu.js +0 -2
  136. package/vendor/codex-taskboard/dist/web/assets/swimlanesDiagram-ULZ7WXOC-CPd58Xso.js +0 -8
  137. package/vendor/codex-taskboard/dist/web/assets/timeline-definition-Z64GVDOM-BUBv8aKP.js +0 -120
  138. package/vendor/codex-taskboard/dist/web/assets/treeView-QDETBFTQ-C-Bve2kY.js +0 -1
  139. package/vendor/codex-taskboard/dist/web/assets/treemap-6X3UGDF4-BcphHDdV.js +0 -1
  140. package/vendor/codex-taskboard/dist/web/assets/vennDiagram-T6HMQDX7-0wQnwAyA.js +0 -34
  141. package/vendor/codex-taskboard/dist/web/assets/wardley-OPB4EBWU-CJZ4BevB.js +0 -1
  142. package/vendor/codex-taskboard/dist/web/assets/wardleyDiagram-T6FBY63Y-U9nwPrkN.js +0 -78
  143. package/vendor/codex-taskboard/dist/web/assets/xychartDiagram-ELKLHX3M-CyCqnzgE.js +0 -7
  144. package/vendor/codex-taskboard/dist/web/codex-agent-logo.png +0 -0
  145. package/vendor/codex-taskboard/dist/web/codex-app-icon.png +0 -0
  146. package/vendor/codex-taskboard/dist/web/favicon.svg +0 -7
  147. package/vendor/codex-taskboard/dist/web/index.html +0 -20
  148. package/vendor/codex-taskboard/server/ai-chat-catalog.mjs +0 -1031
  149. package/vendor/codex-taskboard/server/ai-chat-process.mjs +0 -462
  150. package/vendor/codex-taskboard/server/ai-chat.mjs +0 -1237
  151. package/vendor/codex-taskboard/server/ai-turn-owner.mjs +0 -39
  152. package/vendor/codex-taskboard/server/app.mjs +0 -3211
  153. package/vendor/codex-taskboard/server/cloud-config.mjs +0 -188
  154. package/vendor/codex-taskboard/server/cloud-proxy.mjs +0 -310
  155. package/vendor/codex-taskboard/server/codex-app-server.mjs +0 -376
  156. package/vendor/codex-taskboard/server/codex-slash-commands-0.139.0.json +0 -62
  157. package/vendor/codex-taskboard/server/database.mjs +0 -2845
  158. package/vendor/codex-taskboard/server/index.mjs +0 -41
  159. package/vendor/codex-taskboard/server/jira-config.mjs +0 -181
  160. package/vendor/codex-taskboard/server/jira-integration.mjs +0 -487
  161. package/vendor/codex-taskboard/server/project-summary.mjs +0 -159
  162. package/vendor/codex-taskboard/shared/api-fields.mjs +0 -163
  163. package/vendor/codex-taskboard/shared/codex-environment.mjs +0 -5
  164. package/vendor/codex-taskboard/shared/codex-executable.mjs +0 -77
  165. package/vendor/codex-taskboard/shared/codex-permissions.mjs +0 -9
  166. package/vendor/codex-taskboard/shared/composer-reference.d.mts +0 -19
  167. package/vendor/codex-taskboard/shared/composer-reference.mjs +0 -90
  168. package/vendor/codex-taskboard/shared/domain.mjs +0 -34
  169. package/vendor/codex-taskboard/shared/executable-command.mjs +0 -13
  170. package/vendor/codex-taskboard/shared/process-tree.mjs +0 -27
  171. package/vendor/codex-taskboard/shared/task-input.mjs +0 -308
  172. package/vendor/codex-taskboard/shared/task-records.mjs +0 -199
  173. package/vendor/codex-taskboard/shared/task-relations.mjs +0 -55
  174. package/vendor/codex-taskboard/shared/taskboard-automation.mjs +0 -277
  175. package/vendor/codex-taskboard/skills/manage-taskboard/SKILL.md +0 -45
  176. package/vendor/codex-taskboard/skills/manage-taskboard/agents/openai.yaml +0 -4
  177. package/vendor/codex-taskboard/skills/manage-taskboard/references/cli.md +0 -211
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "d240da17b4bfee37b304e0a51c90506979357d8c",
3
- "builtAt": "2026-09-10T04:56:25.817Z",
4
- "version": "4.57.0",
2
+ "commit": "3ab96a356a555f419adb4ec4c5cba63dfd170a5c",
3
+ "builtAt": "2026-09-11T13:51:01.381Z",
4
+ "version": "4.58.0",
5
5
  "channel": "stable"
6
6
  }
@@ -6,5 +6,4 @@ export interface SpawnHelperRepair {
6
6
  /** Repair one spawn-helper file. Exported for tests. */
7
7
  export declare function repairSpawnHelperFile(vendorPath: string): SpawnHelperRepair;
8
8
  export declare function ensureNodePtyHelperExecutable(): void;
9
- export declare function resetNodePtyHelperStateForTests(): void;
10
9
  export declare function describePtySpawnFailure(error: unknown): string;
@@ -141,11 +141,6 @@ export function ensureNodePtyHelperExecutable() {
141
141
  }
142
142
  }
143
143
  }
144
- export function resetNodePtyHelperStateForTests() {
145
- cachedVendorHelperPath = null;
146
- patchedVendorHelperPath = null;
147
- patchedExecutableHelperPath = null;
148
- }
149
144
  export function describePtySpawnFailure(error) {
150
145
  const message = getErrorMessage(error, "无法启动终端。");
151
146
  if (/posix_spawnp failed/i.test(message)) {
@@ -618,29 +618,33 @@ export async function callConfiguredAiText(prompt, cwd, language, opts) {
618
618
  }
619
619
  return callCliAiText(prompt, cwd, language, opts);
620
620
  }
621
- async function collectStagedDiff(cwd) {
622
- let diff;
623
- try {
624
- diff = await runGitAsync(["diff", "--cached", "--submodule=log"], cwd, 5000);
625
- }
626
- catch {
627
- diff = "";
628
- }
629
- if (!diff) {
621
+ /** Read the unstaged + staged tree without touching the index. */
622
+ async function collectWorkingTreeDiff(cwd) {
623
+ const candidates = [
624
+ [["diff", "HEAD", "--submodule=log"], 5000],
625
+ [["diff", "--cached", "--submodule=log"], 5000],
626
+ [["diff", "HEAD", "--name-only"], 3000],
627
+ ];
628
+ let diff = "";
629
+ for (const [args, timeout] of candidates) {
630
630
  try {
631
- diff = await runGitAsync(["diff", "--cached", "--name-only"], cwd, 3000);
631
+ diff = await runGitAsync(args, cwd, timeout);
632
632
  }
633
633
  catch {
634
- diff = "(no diff available)";
634
+ diff = "";
635
635
  }
636
+ if (diff)
637
+ break;
636
638
  }
639
+ if (!diff)
640
+ diff = "(no diff available)";
637
641
  if (diff.length > MAX_DIFF_FOR_AI) {
638
642
  diff = diff.slice(0, MAX_DIFF_FOR_AI) + "\n\n... (diff truncated) ...";
639
643
  }
640
644
  return diff;
641
645
  }
642
646
  async function generateCommitMessage(cwd, language, ai = {}) {
643
- const diff = await collectStagedDiff(cwd);
647
+ const diff = await collectWorkingTreeDiff(cwd);
644
648
  const lang = language.trim() || "中文";
645
649
  const prompt = `阅读以下 git diff,用${lang}写一条简洁的 commit message。要求:祈使句,不超过 50 字,描述「做了什么」。只输出 message 本身,不要引号、不要 Markdown 格式、不要任何额外说明。\n\n${diff}`;
646
650
  const raw = await callConfiguredAiText(prompt, cwd, language, ai);
@@ -678,7 +682,7 @@ function sanitizeSuggestedTag(value) {
678
682
  return cleaned;
679
683
  }
680
684
  async function generateCommitMessageWithTag(cwd, language, ai = {}) {
681
- const diff = await collectStagedDiff(cwd);
685
+ const diff = await collectWorkingTreeDiff(cwd);
682
686
  let latestTag;
683
687
  try {
684
688
  latestTag = await runGitAsync(["describe", "--tags", "--abbrev=0"], cwd) || undefined;
@@ -721,12 +725,7 @@ export async function generateCommitMessageOnly(cwd, language, ai = {}) {
721
725
  if (!cwd || !existsSync(cwd)) {
722
726
  throw new QuickCommitError("工作目录不存在。", "CWD_MISSING");
723
727
  }
724
- try {
725
- await runGitAsync(["add", "-A"], cwd, 5000);
726
- }
727
- catch {
728
- // best-effort staging so the diff is complete
729
- }
728
+ // Read the working tree as-is. Generating a message must not stage files.
730
729
  return generateCommitMessageWithTag(cwd, language, ai);
731
730
  }
732
731
  /**
@@ -1,6 +1,7 @@
1
1
  import type { WandStorage } from "./storage.js";
2
2
  export declare const GITHUB_PROVIDER = "github";
3
3
  export declare const GITHUB_API_URL = "https://api.github.com";
4
+ export declare const GITHUB_REQUEST_TIMEOUT_MS = 15000;
4
5
  export interface GithubConnectorStatus {
5
6
  provider: "github";
6
7
  connected: boolean;
@@ -14,9 +15,14 @@ export interface GithubApiResponse<T> {
14
15
  data: T;
15
16
  headers: Headers;
16
17
  }
17
- export declare function getGithubApiUrl(storage: WandStorage): string;
18
+ export interface GithubRequestOptions {
19
+ token?: string;
20
+ apiUrl?: string;
21
+ timeoutMs?: number;
22
+ }
18
23
  export declare function getGithubConnectorStatus(storage: WandStorage): GithubConnectorStatus;
19
- export declare function githubRequest<T = unknown>(storage: WandStorage, pathname: string, init?: RequestInit): Promise<GithubApiResponse<T>>;
20
- export declare function connectGithub(storage: WandStorage, token: string, apiUrl?: string): Promise<GithubConnectorStatus>;
21
- export declare function githubStatusWithScopes(storage: WandStorage, scopes?: string[]): GithubConnectorStatus;
22
- export declare function jsonBody(value: unknown): string;
24
+ export declare function githubRequest<T = unknown>(storage: WandStorage, pathname: string, init?: RequestInit, options?: GithubRequestOptions): Promise<GithubApiResponse<T>>;
25
+ export declare function connectGithub(storage: WandStorage, token: string, apiUrl?: string, options?: {
26
+ timeoutMs?: number;
27
+ signal?: AbortSignal;
28
+ }): Promise<GithubConnectorStatus>;
@@ -1,5 +1,8 @@
1
1
  export const GITHUB_PROVIDER = "github";
2
2
  export const GITHUB_API_URL = "https://api.github.com";
3
+ export const GITHUB_REQUEST_TIMEOUT_MS = 15_000;
4
+ let connectEpoch = 0;
5
+ let connectAbort = null;
3
6
  function normalizeApiUrl(value) {
4
7
  const raw = typeof value === "string" && value.trim() ? value.trim() : GITHUB_API_URL;
5
8
  let parsed;
@@ -17,9 +20,6 @@ function normalizeApiUrl(value) {
17
20
  }
18
21
  return parsed.toString().replace(/\/$/, "");
19
22
  }
20
- export function getGithubApiUrl(storage) {
21
- return normalizeApiUrl(storage.getConnectorMeta(GITHUB_PROVIDER)?.apiUrl || GITHUB_API_URL);
22
- }
23
23
  export function getGithubConnectorStatus(storage) {
24
24
  const meta = storage.getConnectorMeta(GITHUB_PROVIDER);
25
25
  const token = storage.getConnectorToken(GITHUB_PROVIDER);
@@ -45,15 +45,52 @@ function apiPath(pathname) {
45
45
  }
46
46
  return pathname;
47
47
  }
48
- export async function githubRequest(storage, pathname, init = {}) {
48
+ function requestTimeoutError() {
49
+ const error = new Error("GitHub API 请求超时。");
50
+ error.status = 504;
51
+ return error;
52
+ }
53
+ /** Reject when the timeout fires, even if a transport resolves after aborting. */
54
+ function timeoutRejection(timeout) {
55
+ return new Promise((_resolve, reject) => {
56
+ if (timeout.aborted) {
57
+ reject(requestTimeoutError());
58
+ return;
59
+ }
60
+ timeout.addEventListener("abort", () => reject(requestTimeoutError()), { once: true });
61
+ });
62
+ }
63
+ function snapshotConnector(storage) {
64
+ return {
65
+ meta: storage.getConnectorMeta(GITHUB_PROVIDER),
66
+ token: storage.getConnectorToken(GITHUB_PROVIDER),
67
+ };
68
+ }
69
+ export async function githubRequest(storage, pathname, init = {}, options) {
70
+ const token = options?.token?.trim() || connectorToken(storage);
71
+ const apiUrl = normalizeApiUrl(options?.apiUrl ?? storage.getConnectorMeta(GITHUB_PROVIDER)?.apiUrl ?? GITHUB_API_URL);
49
72
  const headers = new Headers(init.headers);
50
73
  headers.set("Accept", "application/vnd.github+json");
51
74
  headers.set("X-GitHub-Api-Version", "2022-11-28");
52
75
  headers.set("User-Agent", "wand-github-connector");
53
- headers.set("Authorization", `Bearer ${connectorToken(storage)}`);
76
+ headers.set("Authorization", `Bearer ${token}`);
54
77
  if (init.body !== undefined && !headers.has("Content-Type"))
55
78
  headers.set("Content-Type", "application/json");
56
- const response = await fetch(`${getGithubApiUrl(storage)}${apiPath(pathname)}`, { ...init, headers });
79
+ const timeoutMs = options?.timeoutMs ?? GITHUB_REQUEST_TIMEOUT_MS;
80
+ const timeout = AbortSignal.timeout(timeoutMs);
81
+ const signal = init.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
82
+ let response;
83
+ try {
84
+ response = await Promise.race([
85
+ fetch(`${apiUrl}${apiPath(pathname)}`, { ...init, headers, signal }),
86
+ timeoutRejection(timeout),
87
+ ]);
88
+ }
89
+ catch (error) {
90
+ if (timeout.aborted && !init.signal?.aborted)
91
+ throw requestTimeoutError();
92
+ throw error;
93
+ }
57
94
  const raw = await response.text();
58
95
  let data = null;
59
96
  try {
@@ -66,52 +103,87 @@ export async function githubRequest(storage, pathname, init = {}) {
66
103
  const message = data && typeof data === "object" && "message" in data && typeof data.message === "string"
67
104
  ? data.message
68
105
  : `GitHub API 请求失败(${response.status})。`;
69
- const error = new Error(message);
70
- error.status = response.status;
71
- throw error;
106
+ const requestError = new Error(message);
107
+ requestError.status = response.status;
108
+ throw requestError;
72
109
  }
73
110
  return { data: data, headers: response.headers };
74
111
  }
75
- export async function connectGithub(storage, token, apiUrl) {
112
+ function parseScopes(headers) {
113
+ return (headers.get("x-oauth-scopes") || "")
114
+ .split(",")
115
+ .map((scope) => scope.trim())
116
+ .filter(Boolean);
117
+ }
118
+ function restoreConnector(storage, snapshot) {
119
+ if (snapshot.meta?.connectedAt && snapshot.token) {
120
+ storage.saveConnector(GITHUB_PROVIDER, {
121
+ token: snapshot.token,
122
+ apiUrl: snapshot.meta.apiUrl,
123
+ username: snapshot.meta.username,
124
+ markConnected: true,
125
+ });
126
+ return;
127
+ }
128
+ storage.deleteConnector(GITHUB_PROVIDER);
129
+ }
130
+ /** Undo this attempt's own connector write, without clobbering a newer connect. */
131
+ function rollbackOwnConnectWrite(storage, attempt) {
132
+ if (attempt.epoch !== connectEpoch || !attempt.wrote)
133
+ return;
134
+ const current = snapshotConnector(storage);
135
+ const wroteThisAttempt = current.token === attempt.token && current.meta?.apiUrl === attempt.apiUrl;
136
+ if (!wroteThisAttempt)
137
+ return;
138
+ restoreConnector(storage, attempt.previous);
139
+ }
140
+ export async function connectGithub(storage, token, apiUrl, options) {
76
141
  const normalizedToken = token.trim();
77
142
  if (!normalizedToken)
78
143
  throw new Error("GitHub Token 不能为空。");
79
144
  const targetApiUrl = normalizeApiUrl(apiUrl);
80
- const previous = storage.getConnectorMeta(GITHUB_PROVIDER);
81
- const previousToken = storage.getConnectorToken(GITHUB_PROVIDER);
82
- storage.saveConnector(GITHUB_PROVIDER, { token: normalizedToken, apiUrl: targetApiUrl, markConnected: false });
145
+ const previous = snapshotConnector(storage);
146
+ const epoch = ++connectEpoch;
147
+ connectAbort?.abort();
148
+ const abort = new AbortController();
149
+ connectAbort = abort;
150
+ const signal = options?.signal ? AbortSignal.any([options.signal, abort.signal]) : abort.signal;
151
+ let wrote = false;
83
152
  try {
84
- const profile = await githubRequest(storage, "/user");
153
+ const profile = await githubRequest(storage, "/user", { signal }, {
154
+ token: normalizedToken,
155
+ apiUrl: targetApiUrl,
156
+ timeoutMs: options?.timeoutMs,
157
+ });
85
158
  const username = typeof profile.data.login === "string" ? profile.data.login : "";
86
159
  if (!username)
87
160
  throw new Error("GitHub 返回的账号信息无效。");
88
- const scopes = (profile.headers.get("x-oauth-scopes") || "")
89
- .split(",").map((scope) => scope.trim()).filter(Boolean);
161
+ if (epoch !== connectEpoch)
162
+ throw new Error("GitHub 连接请求已过期。");
163
+ const scopes = parseScopes(profile.headers);
90
164
  const meta = storage.saveConnector(GITHUB_PROVIDER, {
165
+ token: normalizedToken,
91
166
  apiUrl: targetApiUrl,
92
167
  username,
93
168
  markConnected: true,
94
169
  });
170
+ wrote = true;
171
+ if (epoch !== connectEpoch)
172
+ throw new Error("GitHub 连接请求已过期。");
95
173
  return { ...getGithubConnectorStatus(storage), ...meta, provider: "github", connected: true, scopes };
96
174
  }
97
175
  catch (error) {
98
- if (previous?.connectedAt && previousToken) {
99
- storage.saveConnector(GITHUB_PROVIDER, {
100
- token: previousToken,
101
- apiUrl: previous.apiUrl,
102
- username: previous.username,
103
- markConnected: true,
104
- });
105
- }
106
- else {
107
- storage.deleteConnector(GITHUB_PROVIDER);
108
- }
176
+ rollbackOwnConnectWrite(storage, {
177
+ epoch,
178
+ token: normalizedToken,
179
+ apiUrl: targetApiUrl,
180
+ wrote,
181
+ previous,
182
+ });
109
183
  throw error;
110
184
  }
111
- }
112
- export function githubStatusWithScopes(storage, scopes = []) {
113
- return { ...getGithubConnectorStatus(storage), scopes };
114
- }
115
- export function jsonBody(value) {
116
- return JSON.stringify(value);
185
+ finally {
186
+ if (connectAbort === abort)
187
+ connectAbort = null;
188
+ }
117
189
  }
@@ -20,7 +20,7 @@ export declare class Missions {
20
20
  markInboxRead(sessionId?: string): void;
21
21
  diff(missionId: string, attemptId: string): MissionDiff;
22
22
  addReviewComment(missionId: string, attemptId: string, input: CreateReviewCommentInput): MissionReviewComment;
23
- sendReview(missionId: string, attemptId: string, commentIds?: string[]): MissionReviewComment[];
23
+ sendReview(missionId: string, attemptId: string, commentIds?: string[]): Promise<MissionReviewComment[]>;
24
24
  resolveReview(missionId: string, attemptId: string, commentIds: string[]): MissionReviewComment[];
25
25
  archive(id: string): MissionDetails;
26
26
  private dispatchAttempt;
package/dist/missions.js CHANGED
@@ -241,7 +241,7 @@ export class Missions {
241
241
  this.storage.saveMissionReviewComment(comment);
242
242
  return comment;
243
243
  }
244
- sendReview(missionId, attemptId, commentIds) {
244
+ async sendReview(missionId, attemptId, commentIds) {
245
245
  const attempt = this.requireAttempt(missionId, attemptId);
246
246
  if (!attempt.sessionId)
247
247
  throw new Error("这个 attempt 没有关联会话。");
@@ -252,11 +252,24 @@ export class Missions {
252
252
  const session = this.structured.get(attempt.sessionId);
253
253
  if (!session)
254
254
  throw new Error("任务会话当前不可用。");
255
- const completion = this.structured.sendMessage(attempt.sessionId, reviewPrompt(selected));
256
- completion.catch((error) => {
255
+ const commentIdsToSend = selected.map((comment) => comment.id);
256
+ const accepted = this.structured.sendMessage(attempt.sessionId, reviewPrompt(selected));
257
+ // sendMessage settles after the whole turn. Treat microtask acceptance as
258
+ // "queued or started"; a synchronous/microtask rejection stays pending.
259
+ try {
260
+ await Promise.race([
261
+ accepted,
262
+ new Promise((resolve) => setImmediate(resolve)),
263
+ ]);
264
+ }
265
+ catch (error) {
257
266
  console.error(`[Missions] Review dispatch failed for ${attempt.id}:`, error);
267
+ throw error;
268
+ }
269
+ accepted.catch((error) => {
270
+ console.error(`[Missions] Review execution failed for ${attempt.id}:`, error);
258
271
  });
259
- this.storage.updateMissionReviewStatus(selected.map((comment) => comment.id), "sent");
272
+ this.storage.updateMissionReviewStatus(commentIdsToSend, "sent");
260
273
  this.storage.saveMissionAttempt({ ...attempt, state: "working", updatedAt: nowIso() });
261
274
  this.refreshMissionStatus(missionId);
262
275
  return this.storage.listMissionReviewComments(missionId, attemptId);
@@ -129,7 +129,7 @@ export function registerFileRoutes(app, deps) {
129
129
  size: fileStat.size,
130
130
  mime,
131
131
  };
132
- res.json(payload);
132
+ res.json({ ...payload, mtime: fileStat.mtime.toISOString() });
133
133
  return;
134
134
  }
135
135
  if (fileStat.size > MAX_TEXT_PREVIEW_SIZE) {
@@ -152,7 +152,7 @@ export function registerFileRoutes(app, deps) {
152
152
  lang: getLanguageFromExt(ext, filePath),
153
153
  content,
154
154
  };
155
- res.json(payload);
155
+ res.json({ ...payload, mtime: fileStat.mtime.toISOString() });
156
156
  }
157
157
  catch (error) {
158
158
  res.status(400).json({ error: getErrorMessage(error, "Failed to read file") });
@@ -196,6 +196,16 @@ export function registerFileRoutes(app, deps) {
196
196
  res.status(415).json({ error: "仅支持编辑文本类文件。" });
197
197
  return;
198
198
  }
199
+ if (fileChangedSince(body.expectedMtime, body.expectedSize, fileStat)) {
200
+ res.status(409).json({
201
+ error: "文件已被外部修改。",
202
+ errorCode: "FILE_CHANGED",
203
+ path: resolvedPath,
204
+ size: fileStat.size,
205
+ mtime: fileStat.mtime.toISOString(),
206
+ });
207
+ return;
208
+ }
199
209
  const tmpPath = path.join(path.dirname(resolvedPath), `.${baseName}.wand-tmp-${crypto.randomBytes(6).toString("hex")}`);
200
210
  try {
201
211
  await writeFile(tmpPath, content, { encoding: "utf-8", mode: fileStat.mode & 0o777 });
@@ -732,6 +742,37 @@ const RAW_MAX_BYTES_BY_KIND = {
732
742
  audio: 200 * 1024 * 1024,
733
743
  binary: 50 * 1024 * 1024,
734
744
  };
745
+ /** Compare an optional client baseline (mtime and/or size) against the live file. */
746
+ function fileChangedSince(expectedMtime, expectedSize, actual) {
747
+ const mtime = parseExpectedMtime(expectedMtime);
748
+ if (mtime && !mtimeEquals(mtime, actual.mtime))
749
+ return true;
750
+ const size = parseExpectedSize(expectedSize);
751
+ return size !== undefined && size !== actual.size;
752
+ }
753
+ function parseExpectedMtime(value) {
754
+ if (typeof value === "string")
755
+ return value.trim();
756
+ if (typeof value === "number" && Number.isFinite(value))
757
+ return new Date(value).toISOString();
758
+ return "";
759
+ }
760
+ function parseExpectedSize(value) {
761
+ if (typeof value === "number" && Number.isFinite(value))
762
+ return value;
763
+ if (typeof value === "string" && value.trim() !== "") {
764
+ const parsed = Number(value);
765
+ if (Number.isFinite(parsed))
766
+ return parsed;
767
+ }
768
+ return undefined;
769
+ }
770
+ function mtimeEquals(expected, actual) {
771
+ if (expected === actual.toISOString())
772
+ return true;
773
+ const parsed = Date.parse(expected);
774
+ return Number.isFinite(parsed) && parsed === actual.getTime();
775
+ }
735
776
  function classifyFile(ext, baseName) {
736
777
  const lowerExt = ext.toLowerCase();
737
778
  const lowerBase = baseName.toLowerCase();
@@ -1,5 +1,5 @@
1
1
  import { asyncRoute } from "./express-async.js";
2
- import { connectGithub, githubRequest, githubStatusWithScopes, jsonBody, } from "./github-connector.js";
2
+ import { connectGithub, getGithubConnectorStatus, githubRequest, } from "./github-connector.js";
3
3
  import { getErrorMessage } from "./error-utils.js";
4
4
  function stringQuery(value, fallback = "") {
5
5
  return typeof value === "string" ? value.trim() : fallback;
@@ -8,6 +8,22 @@ function integerQuery(value, fallback, min, max) {
8
8
  const parsed = Number(stringQuery(value));
9
9
  return Number.isInteger(parsed) && parsed >= min && parsed <= max ? parsed : fallback;
10
10
  }
11
+ function githubListQuery(reqQuery, defaults = {}) {
12
+ const query = new URLSearchParams();
13
+ if (defaults.state)
14
+ query.set("state", stringQuery(reqQuery.state, defaults.state));
15
+ if (stringQuery(reqQuery.labels))
16
+ query.set("labels", stringQuery(reqQuery.labels));
17
+ const sort = stringQuery(reqQuery.sort, defaults.sort ?? "");
18
+ const direction = stringQuery(reqQuery.direction, defaults.direction ?? "");
19
+ if (sort)
20
+ query.set("sort", sort);
21
+ if (direction)
22
+ query.set("direction", direction);
23
+ query.set("per_page", String(integerQuery(reqQuery.per_page, 30, 1, 100)));
24
+ query.set("page", String(integerQuery(reqQuery.page, 1, 1, 10_000)));
25
+ return query;
26
+ }
11
27
  function repoPath(req) {
12
28
  const owner = req.params.owner?.trim();
13
29
  const repo = req.params.repo?.trim();
@@ -39,11 +55,19 @@ function publicError(error) {
39
55
  : 400;
40
56
  return { status: status >= 400 && status < 600 ? status : 500, message: getErrorMessage(error, "GitHub 操作失败。") };
41
57
  }
58
+ function sendGithubError(res, error, remapAuth = false) {
59
+ const result = publicError(error);
60
+ const status = remapAuth && (result.status === 401 || result.status === 403) ? 400 : result.status;
61
+ res.status(status).json({ error: result.message });
62
+ }
63
+ async function githubJson(storage, pathname, init = {}) {
64
+ return (await githubRequest(storage, pathname, init)).data;
65
+ }
42
66
  export function registerGithubRoutes(app, deps) {
43
67
  const { storage, requireAdmin, sessions } = deps;
44
68
  app.get("/api/connectors/github", requireAdmin, (_req, res) => {
45
69
  res.set("Cache-Control", "no-store");
46
- res.json(githubStatusWithScopes(storage));
70
+ res.json(getGithubConnectorStatus(storage));
47
71
  });
48
72
  app.post("/api/connectors/github", requireAdmin, asyncRoute(async (req, res) => {
49
73
  try {
@@ -54,8 +78,7 @@ export function registerGithubRoutes(app, deps) {
54
78
  res.json(await connectGithub(storage, token, apiUrl));
55
79
  }
56
80
  catch (error) {
57
- const result = publicError(error);
58
- res.status(result.status === 401 || result.status === 403 ? 400 : result.status).json({ error: result.message });
81
+ sendGithubError(res, error, true);
59
82
  }
60
83
  }));
61
84
  app.delete("/api/connectors/github", requireAdmin, (_req, res) => {
@@ -69,8 +92,7 @@ export function registerGithubRoutes(app, deps) {
69
92
  res.json({ bindings });
70
93
  }
71
94
  catch (error) {
72
- const result = publicError(error);
73
- res.status(result.status).json({ error: result.message });
95
+ sendGithubError(res, error);
74
96
  }
75
97
  });
76
98
  app.post("/api/github/repos/:owner/:repo/issues/:number/bindings", requireAdmin, (req, res) => {
@@ -84,8 +106,7 @@ export function registerGithubRoutes(app, deps) {
84
106
  res.status(201).json({ ok: true, sessionId });
85
107
  }
86
108
  catch (error) {
87
- const result = publicError(error);
88
- res.status(result.status).json({ error: result.message });
109
+ sendGithubError(res, error);
89
110
  }
90
111
  });
91
112
  app.delete("/api/github/repos/:owner/:repo/issues/:number/bindings/:sessionId", requireAdmin, (req, res) => {
@@ -94,17 +115,15 @@ export function registerGithubRoutes(app, deps) {
94
115
  res.json({ ok: true });
95
116
  }
96
117
  catch (error) {
97
- const result = publicError(error);
98
- res.status(result.status).json({ error: result.message });
118
+ sendGithubError(res, error);
99
119
  }
100
120
  });
101
121
  app.get("/api/github/user", requireAdmin, asyncRoute(async (_req, res) => {
102
122
  try {
103
- res.json((await githubRequest(storage, "/user")).data);
123
+ res.json(await githubJson(storage, "/user"));
104
124
  }
105
125
  catch (error) {
106
- const result = publicError(error);
107
- res.status(result.status).json({ error: result.message });
126
+ sendGithubError(res, error);
108
127
  }
109
128
  }));
110
129
  app.get("/api/github/repos", requireAdmin, asyncRoute(async (req, res) => {
@@ -124,39 +143,27 @@ export function registerGithubRoutes(app, deps) {
124
143
  query.set("per_page", String(integerQuery(req.query.per_page, 30, 1, 100)));
125
144
  query.set("page", String(integerQuery(req.query.page, 1, 1, 10_000)));
126
145
  const suffix = query.toString();
127
- res.json((await githubRequest(storage, `/user/repos?${suffix}`)).data);
146
+ res.json(await githubJson(storage, `/user/repos?${suffix}`));
128
147
  }
129
148
  catch (error) {
130
- const result = publicError(error);
131
- res.status(result.status).json({ error: result.message });
149
+ sendGithubError(res, error);
132
150
  }
133
151
  }));
134
152
  app.get("/api/github/repos/:owner/:repo", requireAdmin, asyncRoute(async (req, res) => {
135
153
  try {
136
- res.json((await githubRequest(storage, repoPath(req))).data);
154
+ res.json(await githubJson(storage, repoPath(req)));
137
155
  }
138
156
  catch (error) {
139
- const result = publicError(error);
140
- res.status(result.status).json({ error: result.message });
157
+ sendGithubError(res, error);
141
158
  }
142
159
  }));
143
160
  app.get("/api/github/repos/:owner/:repo/issues", requireAdmin, asyncRoute(async (req, res) => {
144
161
  try {
145
- const query = new URLSearchParams();
146
- query.set("state", stringQuery(req.query.state, "open"));
147
- if (stringQuery(req.query.labels))
148
- query.set("labels", stringQuery(req.query.labels));
149
- if (stringQuery(req.query.sort))
150
- query.set("sort", stringQuery(req.query.sort));
151
- if (stringQuery(req.query.direction))
152
- query.set("direction", stringQuery(req.query.direction));
153
- query.set("per_page", String(integerQuery(req.query.per_page, 30, 1, 100)));
154
- query.set("page", String(integerQuery(req.query.page, 1, 1, 10_000)));
155
- res.json((await githubRequest(storage, `${repoPath(req)}/issues?${query}`)).data);
162
+ const query = githubListQuery(req.query, { state: "open" });
163
+ res.json(await githubJson(storage, `${repoPath(req)}/issues?${query}`));
156
164
  }
157
165
  catch (error) {
158
- const result = publicError(error);
159
- res.status(result.status).json({ error: result.message });
166
+ sendGithubError(res, error);
160
167
  }
161
168
  }));
162
169
  app.post("/api/github/repos/:owner/:repo/issues", requireAdmin, asyncRoute(async (req, res) => {
@@ -167,11 +174,10 @@ export function registerGithubRoutes(app, deps) {
167
174
  if (body[key] !== undefined)
168
175
  payload[key] = body[key];
169
176
  }
170
- res.status(201).json((await githubRequest(storage, `${repoPath(req)}/issues`, { method: "POST", body: jsonBody(payload) })).data);
177
+ res.status(201).json(await githubJson(storage, `${repoPath(req)}/issues`, { method: "POST", body: JSON.stringify(payload) }));
171
178
  }
172
179
  catch (error) {
173
- const result = publicError(error);
174
- res.status(result.status).json({ error: result.message });
180
+ sendGithubError(res, error);
175
181
  }
176
182
  }));
177
183
  app.patch("/api/github/repos/:owner/:repo/issues/:number", requireAdmin, asyncRoute(async (req, res) => {
@@ -184,37 +190,29 @@ export function registerGithubRoutes(app, deps) {
184
190
  }
185
191
  if (!Object.keys(payload).length)
186
192
  throw new Error("没有可更新的 Issue 字段。");
187
- res.json((await githubRequest(storage, `${repoPath(req)}/issues/${issueNumber(req)}`, { method: "PATCH", body: jsonBody(payload) })).data);
193
+ res.json(await githubJson(storage, `${repoPath(req)}/issues/${issueNumber(req)}`, { method: "PATCH", body: JSON.stringify(payload) }));
188
194
  }
189
195
  catch (error) {
190
- const result = publicError(error);
191
- res.status(result.status).json({ error: result.message });
196
+ sendGithubError(res, error);
192
197
  }
193
198
  }));
194
199
  app.post("/api/github/repos/:owner/:repo/issues/:number/comments", requireAdmin, asyncRoute(async (req, res) => {
195
200
  try {
196
201
  const body = validateObjectBody(req.body);
197
202
  const payload = { body: requiredString(body, "body") };
198
- res.status(201).json((await githubRequest(storage, `${repoPath(req)}/issues/${issueNumber(req)}/comments`, { method: "POST", body: jsonBody(payload) })).data);
203
+ res.status(201).json(await githubJson(storage, `${repoPath(req)}/issues/${issueNumber(req)}/comments`, { method: "POST", body: JSON.stringify(payload) }));
199
204
  }
200
205
  catch (error) {
201
- const result = publicError(error);
202
- res.status(result.status).json({ error: result.message });
206
+ sendGithubError(res, error);
203
207
  }
204
208
  }));
205
209
  app.get("/api/github/repos/:owner/:repo/pulls", requireAdmin, asyncRoute(async (req, res) => {
206
210
  try {
207
- const query = new URLSearchParams();
208
- query.set("state", stringQuery(req.query.state, "open"));
209
- query.set("sort", stringQuery(req.query.sort, "created"));
210
- query.set("direction", stringQuery(req.query.direction, "desc"));
211
- query.set("per_page", String(integerQuery(req.query.per_page, 30, 1, 100)));
212
- query.set("page", String(integerQuery(req.query.page, 1, 1, 10_000)));
213
- res.json((await githubRequest(storage, `${repoPath(req)}/pulls?${query}`)).data);
211
+ const query = githubListQuery(req.query, { state: "open", sort: "created", direction: "desc" });
212
+ res.json(await githubJson(storage, `${repoPath(req)}/pulls?${query}`));
214
213
  }
215
214
  catch (error) {
216
- const result = publicError(error);
217
- res.status(result.status).json({ error: result.message });
215
+ sendGithubError(res, error);
218
216
  }
219
217
  }));
220
218
  app.post("/api/github/repos/:owner/:repo/pulls", requireAdmin, asyncRoute(async (req, res) => {
@@ -229,11 +227,10 @@ export function registerGithubRoutes(app, deps) {
229
227
  if (body[key] !== undefined)
230
228
  payload[key] = body[key];
231
229
  }
232
- res.status(201).json((await githubRequest(storage, `${repoPath(req)}/pulls`, { method: "POST", body: jsonBody(payload) })).data);
230
+ res.status(201).json(await githubJson(storage, `${repoPath(req)}/pulls`, { method: "POST", body: JSON.stringify(payload) }));
233
231
  }
234
232
  catch (error) {
235
- const result = publicError(error);
236
- res.status(result.status).json({ error: result.message });
233
+ sendGithubError(res, error);
237
234
  }
238
235
  }));
239
236
  }