@ipul-labs/forge 1.16.8 → 1.17.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 (280) hide show
  1. package/README.md +39 -21
  2. package/lib/bin/cli/analyze.d.ts +9 -0
  3. package/lib/bin/cli/analyze.d.ts.map +1 -0
  4. package/lib/bin/cli/analyze.js +491 -0
  5. package/lib/bin/cli/analyze.js.map +1 -0
  6. package/lib/bin/cli/apply.d.ts +2 -0
  7. package/lib/bin/cli/apply.d.ts.map +1 -0
  8. package/lib/bin/cli/apply.js +262 -0
  9. package/lib/bin/cli/apply.js.map +1 -0
  10. package/lib/bin/cli/cifix.d.ts +2 -0
  11. package/lib/bin/cli/cifix.d.ts.map +1 -0
  12. package/lib/bin/cli/cifix.js +310 -0
  13. package/lib/bin/cli/cifix.js.map +1 -0
  14. package/lib/bin/cli/close.d.ts +2 -0
  15. package/lib/bin/cli/close.d.ts.map +1 -0
  16. package/lib/bin/cli/close.js +113 -0
  17. package/lib/bin/cli/close.js.map +1 -0
  18. package/lib/bin/cli/createIssue.d.ts +2 -0
  19. package/lib/bin/cli/createIssue.d.ts.map +1 -0
  20. package/lib/bin/cli/createIssue.js +108 -0
  21. package/lib/bin/cli/createIssue.js.map +1 -0
  22. package/lib/bin/cli/doctor.d.ts +2 -0
  23. package/lib/bin/cli/doctor.d.ts.map +1 -0
  24. package/lib/bin/cli/doctor.js +145 -0
  25. package/lib/bin/cli/doctor.js.map +1 -0
  26. package/lib/bin/cli/init.d.ts +4 -0
  27. package/lib/bin/cli/init.d.ts.map +1 -0
  28. package/lib/bin/cli/init.js +288 -0
  29. package/lib/bin/cli/init.js.map +1 -0
  30. package/lib/bin/cli/ink.d.ts +23 -0
  31. package/lib/bin/cli/ink.d.ts.map +1 -0
  32. package/{bin → lib/bin}/cli/ink.js +18 -19
  33. package/lib/bin/cli/ink.js.map +1 -0
  34. package/lib/bin/cli/issue.d.ts +2 -0
  35. package/lib/bin/cli/issue.d.ts.map +1 -0
  36. package/lib/bin/cli/issue.js +114 -0
  37. package/lib/bin/cli/issue.js.map +1 -0
  38. package/lib/bin/cli/issues.d.ts +2 -0
  39. package/lib/bin/cli/issues.d.ts.map +1 -0
  40. package/lib/bin/cli/issues.js +149 -0
  41. package/lib/bin/cli/issues.js.map +1 -0
  42. package/lib/bin/cli/merge.d.ts +2 -0
  43. package/lib/bin/cli/merge.d.ts.map +1 -0
  44. package/lib/bin/cli/merge.js +162 -0
  45. package/lib/bin/cli/merge.js.map +1 -0
  46. package/lib/bin/cli/refine.d.ts +2 -0
  47. package/lib/bin/cli/refine.d.ts.map +1 -0
  48. package/lib/bin/cli/refine.js +249 -0
  49. package/lib/bin/cli/refine.js.map +1 -0
  50. package/lib/bin/cli/repl.d.ts +2 -0
  51. package/lib/bin/cli/repl.d.ts.map +1 -0
  52. package/lib/bin/cli/repl.js +309 -0
  53. package/lib/bin/cli/repl.js.map +1 -0
  54. package/lib/bin/cli/repos.d.ts +2 -0
  55. package/lib/bin/cli/repos.d.ts.map +1 -0
  56. package/lib/bin/cli/repos.js +86 -0
  57. package/lib/bin/cli/repos.js.map +1 -0
  58. package/lib/bin/cli/resolution.d.ts +2 -0
  59. package/lib/bin/cli/resolution.d.ts.map +1 -0
  60. package/lib/bin/cli/resolution.js +139 -0
  61. package/lib/bin/cli/resolution.js.map +1 -0
  62. package/lib/bin/cli/serverClient.d.ts +26 -0
  63. package/lib/bin/cli/serverClient.d.ts.map +1 -0
  64. package/lib/bin/cli/serverClient.js +111 -0
  65. package/lib/bin/cli/serverClient.js.map +1 -0
  66. package/lib/bin/cli/session.d.ts +8 -0
  67. package/lib/bin/cli/session.d.ts.map +1 -0
  68. package/lib/bin/cli/session.js +212 -0
  69. package/lib/bin/cli/session.js.map +1 -0
  70. package/lib/bin/cli/sessionStore.d.ts +69 -0
  71. package/lib/bin/cli/sessionStore.d.ts.map +1 -0
  72. package/lib/bin/cli/sessionStore.js +196 -0
  73. package/lib/bin/cli/sessionStore.js.map +1 -0
  74. package/lib/bin/cli/ship.d.ts +33 -0
  75. package/lib/bin/cli/ship.d.ts.map +1 -0
  76. package/lib/bin/cli/ship.js +737 -0
  77. package/lib/bin/cli/ship.js.map +1 -0
  78. package/lib/bin/cli/types.d.ts +59 -0
  79. package/lib/bin/cli/types.d.ts.map +1 -0
  80. package/lib/bin/cli/types.js +2 -0
  81. package/lib/bin/cli/types.js.map +1 -0
  82. package/lib/bin/cli/update.d.ts +2 -0
  83. package/lib/bin/cli/update.d.ts.map +1 -0
  84. package/lib/bin/cli/update.js +87 -0
  85. package/lib/bin/cli/update.js.map +1 -0
  86. package/lib/bin/cli/utils.d.ts +43 -0
  87. package/lib/bin/cli/utils.d.ts.map +1 -0
  88. package/lib/bin/cli/utils.js +171 -0
  89. package/lib/bin/cli/utils.js.map +1 -0
  90. package/lib/bin/cli/validate.d.ts +2 -0
  91. package/lib/bin/cli/validate.d.ts.map +1 -0
  92. package/lib/bin/cli/validate.js +283 -0
  93. package/lib/bin/cli/validate.js.map +1 -0
  94. package/lib/bin/cli/watch.d.ts +5 -0
  95. package/lib/bin/cli/watch.d.ts.map +1 -0
  96. package/lib/bin/cli/watch.js +263 -0
  97. package/lib/bin/cli/watch.js.map +1 -0
  98. package/lib/bin/forge-agent.d.ts +3 -0
  99. package/lib/bin/forge-agent.d.ts.map +1 -0
  100. package/{bin → lib/bin}/forge-agent.js +104 -105
  101. package/lib/bin/forge-agent.js.map +1 -0
  102. package/lib/server/forge/core/ForgeAgent.d.ts +260 -0
  103. package/lib/server/forge/core/ForgeAgent.d.ts.map +1 -0
  104. package/lib/server/forge/core/ForgeAgent.js +995 -0
  105. package/lib/server/forge/core/ForgeAgent.js.map +1 -0
  106. package/lib/server/forge/core/agentClient.d.ts +39 -0
  107. package/lib/server/forge/core/agentClient.d.ts.map +1 -0
  108. package/{server → lib/server}/forge/core/agentClient.js +7 -12
  109. package/lib/server/forge/core/agentClient.js.map +1 -0
  110. package/lib/server/forge/core/branchName.d.ts +20 -0
  111. package/lib/server/forge/core/branchName.d.ts.map +1 -0
  112. package/lib/server/forge/core/branchName.js +39 -0
  113. package/lib/server/forge/core/branchName.js.map +1 -0
  114. package/lib/server/forge/core/claudeClient.d.ts +60 -0
  115. package/lib/server/forge/core/claudeClient.d.ts.map +1 -0
  116. package/lib/server/forge/core/claudeClient.js +347 -0
  117. package/lib/server/forge/core/claudeClient.js.map +1 -0
  118. package/lib/server/forge/core/configStore.d.ts +76 -0
  119. package/lib/server/forge/core/configStore.d.ts.map +1 -0
  120. package/lib/server/forge/core/configStore.js +213 -0
  121. package/lib/server/forge/core/configStore.js.map +1 -0
  122. package/lib/server/forge/core/gitlabClient.d.ts +144 -0
  123. package/lib/server/forge/core/gitlabClient.d.ts.map +1 -0
  124. package/{server → lib/server}/forge/core/gitlabClient.js +98 -180
  125. package/lib/server/forge/core/gitlabClient.js.map +1 -0
  126. package/lib/server/forge/core/jobs.d.ts +67 -0
  127. package/lib/server/forge/core/jobs.d.ts.map +1 -0
  128. package/lib/server/forge/core/jobs.js +185 -0
  129. package/lib/server/forge/core/jobs.js.map +1 -0
  130. package/lib/server/forge/core/k8s.d.ts +18 -0
  131. package/lib/server/forge/core/k8s.d.ts.map +1 -0
  132. package/lib/server/forge/core/k8s.js +49 -0
  133. package/lib/server/forge/core/k8s.js.map +1 -0
  134. package/lib/server/forge/core/localRepo.d.ts +36 -0
  135. package/lib/server/forge/core/localRepo.d.ts.map +1 -0
  136. package/lib/server/forge/core/localRepo.js +165 -0
  137. package/lib/server/forge/core/localRepo.js.map +1 -0
  138. package/lib/server/forge/core/openclawClient.d.ts +61 -0
  139. package/lib/server/forge/core/openclawClient.d.ts.map +1 -0
  140. package/lib/server/forge/core/openclawClient.js +111 -0
  141. package/lib/server/forge/core/openclawClient.js.map +1 -0
  142. package/lib/server/forge/core/prompts.d.ts +96 -0
  143. package/lib/server/forge/core/prompts.d.ts.map +1 -0
  144. package/lib/server/forge/core/prompts.js +450 -0
  145. package/lib/server/forge/core/prompts.js.map +1 -0
  146. package/lib/server/forge/core/providers/azuredevops.d.ts +134 -0
  147. package/lib/server/forge/core/providers/azuredevops.d.ts.map +1 -0
  148. package/lib/server/forge/core/providers/azuredevops.js +360 -0
  149. package/lib/server/forge/core/providers/azuredevops.js.map +1 -0
  150. package/lib/server/forge/core/providers/base.d.ts +114 -0
  151. package/lib/server/forge/core/providers/base.d.ts.map +1 -0
  152. package/lib/server/forge/core/providers/base.js +98 -0
  153. package/lib/server/forge/core/providers/base.js.map +1 -0
  154. package/lib/server/forge/core/providers/github.d.ts +133 -0
  155. package/lib/server/forge/core/providers/github.d.ts.map +1 -0
  156. package/lib/server/forge/core/providers/github.js +307 -0
  157. package/lib/server/forge/core/providers/github.js.map +1 -0
  158. package/lib/server/forge/core/providers/gitlab.d.ts +144 -0
  159. package/lib/server/forge/core/providers/gitlab.d.ts.map +1 -0
  160. package/lib/server/forge/core/providers/gitlab.js +407 -0
  161. package/lib/server/forge/core/providers/gitlab.js.map +1 -0
  162. package/lib/server/forge/core/providers/index.d.ts +21 -0
  163. package/lib/server/forge/core/providers/index.d.ts.map +1 -0
  164. package/lib/server/forge/core/providers/index.js +43 -0
  165. package/lib/server/forge/core/providers/index.js.map +1 -0
  166. package/lib/server/forge/core/providers/types.d.ts +80 -0
  167. package/lib/server/forge/core/providers/types.d.ts.map +1 -0
  168. package/lib/server/forge/core/providers/types.js +2 -0
  169. package/lib/server/forge/core/providers/types.js.map +1 -0
  170. package/lib/server/forge/core/quotaClient.d.ts +48 -0
  171. package/lib/server/forge/core/quotaClient.d.ts.map +1 -0
  172. package/lib/server/forge/core/quotaClient.js +88 -0
  173. package/lib/server/forge/core/quotaClient.js.map +1 -0
  174. package/lib/server/forge/core/types.d.ts +160 -0
  175. package/lib/server/forge/core/types.d.ts.map +1 -0
  176. package/lib/server/forge/core/types.js +2 -0
  177. package/lib/server/forge/core/types.js.map +1 -0
  178. package/lib/server/forge/core/validator/attribute.d.ts +31 -0
  179. package/lib/server/forge/core/validator/attribute.d.ts.map +1 -0
  180. package/lib/server/forge/core/validator/attribute.js +82 -0
  181. package/lib/server/forge/core/validator/attribute.js.map +1 -0
  182. package/lib/server/forge/core/validator/clone.d.ts +14 -0
  183. package/lib/server/forge/core/validator/clone.d.ts.map +1 -0
  184. package/lib/server/forge/core/validator/clone.js +99 -0
  185. package/lib/server/forge/core/validator/clone.js.map +1 -0
  186. package/lib/server/forge/core/validator/cmd.d.ts +23 -0
  187. package/lib/server/forge/core/validator/cmd.d.ts.map +1 -0
  188. package/lib/server/forge/core/validator/cmd.js +87 -0
  189. package/lib/server/forge/core/validator/cmd.js.map +1 -0
  190. package/lib/server/forge/core/validator/fs.d.ts +5 -0
  191. package/lib/server/forge/core/validator/fs.d.ts.map +1 -0
  192. package/{server → lib/server}/forge/core/validator/fs.js +8 -3
  193. package/lib/server/forge/core/validator/fs.js.map +1 -0
  194. package/lib/server/forge/core/validator/overlay.d.ts +13 -0
  195. package/lib/server/forge/core/validator/overlay.d.ts.map +1 -0
  196. package/lib/server/forge/core/validator/overlay.js +53 -0
  197. package/lib/server/forge/core/validator/overlay.js.map +1 -0
  198. package/lib/server/forge/core/validator/projectType.d.ts +20 -0
  199. package/lib/server/forge/core/validator/projectType.d.ts.map +1 -0
  200. package/lib/server/forge/core/validator/projectType.js +94 -0
  201. package/lib/server/forge/core/validator/projectType.js.map +1 -0
  202. package/lib/server/forge/core/validator/runners/go.d.ts +21 -0
  203. package/lib/server/forge/core/validator/runners/go.d.ts.map +1 -0
  204. package/lib/server/forge/core/validator/runners/go.js +99 -0
  205. package/lib/server/forge/core/validator/runners/go.js.map +1 -0
  206. package/lib/server/forge/core/validator/runners/node.d.ts +12 -0
  207. package/lib/server/forge/core/validator/runners/node.d.ts.map +1 -0
  208. package/lib/server/forge/core/validator/runners/node.js +75 -0
  209. package/lib/server/forge/core/validator/runners/node.js.map +1 -0
  210. package/lib/server/forge/core/validator/tdd.d.ts +30 -0
  211. package/lib/server/forge/core/validator/tdd.d.ts.map +1 -0
  212. package/lib/server/forge/core/validator/tdd.js +96 -0
  213. package/lib/server/forge/core/validator/tdd.js.map +1 -0
  214. package/lib/server/forge/core/validator.d.ts +58 -0
  215. package/lib/server/forge/core/validator.d.ts.map +1 -0
  216. package/lib/server/forge/core/validator.js +150 -0
  217. package/lib/server/forge/core/validator.js.map +1 -0
  218. package/lib/server/forge/routes.d.ts +22 -0
  219. package/lib/server/forge/routes.d.ts.map +1 -0
  220. package/lib/server/forge/routes.js +984 -0
  221. package/lib/server/forge/routes.js.map +1 -0
  222. package/lib/server.d.ts +2 -0
  223. package/lib/server.d.ts.map +1 -0
  224. package/lib/server.js +52 -0
  225. package/lib/server.js.map +1 -0
  226. package/lib/shared/events.d.ts +12 -0
  227. package/lib/shared/events.d.ts.map +1 -0
  228. package/lib/shared/events.js +12 -0
  229. package/lib/shared/events.js.map +1 -0
  230. package/package.json +15 -13
  231. package/scripts/fix-shebang.cjs +10 -0
  232. package/bin/cli/analyze.js +0 -516
  233. package/bin/cli/apply.js +0 -264
  234. package/bin/cli/cifix.js +0 -300
  235. package/bin/cli/close.js +0 -125
  236. package/bin/cli/createIssue.js +0 -112
  237. package/bin/cli/doctor.js +0 -161
  238. package/bin/cli/init.js +0 -306
  239. package/bin/cli/issue.js +0 -116
  240. package/bin/cli/issues.js +0 -155
  241. package/bin/cli/merge.js +0 -175
  242. package/bin/cli/refine.js +0 -253
  243. package/bin/cli/repl.js +0 -301
  244. package/bin/cli/repos.js +0 -91
  245. package/bin/cli/resolution.js +0 -126
  246. package/bin/cli/session.js +0 -230
  247. package/bin/cli/sessionStore.js +0 -211
  248. package/bin/cli/ship.js +0 -762
  249. package/bin/cli/update.js +0 -95
  250. package/bin/cli/utils.js +0 -145
  251. package/bin/cli/validate.js +0 -280
  252. package/bin/cli/watch.js +0 -274
  253. package/server/forge/core/ForgeAgent.js +0 -1179
  254. package/server/forge/core/ForgeAgent.test.js +0 -179
  255. package/server/forge/core/branchName.js +0 -41
  256. package/server/forge/core/claudeClient.js +0 -378
  257. package/server/forge/core/configStore.js +0 -229
  258. package/server/forge/core/configStore.test.js +0 -89
  259. package/server/forge/core/jobs.js +0 -180
  260. package/server/forge/core/k8s.js +0 -54
  261. package/server/forge/core/localRepo.js +0 -163
  262. package/server/forge/core/openclawClient.js +0 -125
  263. package/server/forge/core/prompts.js +0 -483
  264. package/server/forge/core/prompts.test.js +0 -219
  265. package/server/forge/core/providers/azuredevops.js +0 -409
  266. package/server/forge/core/providers/base.js +0 -208
  267. package/server/forge/core/providers/github.js +0 -348
  268. package/server/forge/core/providers/gitlab.js +0 -529
  269. package/server/forge/core/providers/index.js +0 -47
  270. package/server/forge/core/quotaClient.js +0 -101
  271. package/server/forge/core/validator/attribute.js +0 -84
  272. package/server/forge/core/validator/clone.js +0 -116
  273. package/server/forge/core/validator/cmd.js +0 -98
  274. package/server/forge/core/validator/overlay.js +0 -52
  275. package/server/forge/core/validator/projectType.js +0 -87
  276. package/server/forge/core/validator/runners/go.js +0 -107
  277. package/server/forge/core/validator/runners/node.js +0 -84
  278. package/server/forge/core/validator/tdd.js +0 -105
  279. package/server/forge/core/validator.js +0 -167
  280. package/server/forge/routes.js +0 -1027
@@ -0,0 +1,347 @@
1
+ /**
2
+ * server/forge/core/claudeClient.js
3
+ *
4
+ * Shells out to the `claude` CLI (Claude Code) instead of the Anthropic SDK.
5
+ * Uses your Claude Code subscription — no separate API credits needed.
6
+ *
7
+ * Requires: `claude` CLI installed and authenticated (`claude auth login`)
8
+ *
9
+ * Quota headers are NOT available via the CLI — see `quotaClient.js` for a
10
+ * user-triggered probe that hits the Messages API once to read them.
11
+ */
12
+ import { spawn } from "child_process";
13
+ import { SELECT_FILES_SCHEMA, PROPOSE_FIX_SCHEMA, CI_FIX_SCHEMA, REFINE_OR_ASK_SCHEMA, RESOLUTION_CHECK_SCHEMA, buildIdentifyFilesPrompt, buildProposeFixPrompt, buildRefineOrAskPrompt, buildRefineResumePrompt, buildCheckResolutionPrompt, classifyIssue, } from "./prompts.js";
14
+ // FORGE_MODEL env var overrides the default. Set to a cheaper model to reduce cost.
15
+ // e.g. FORGE_MODEL=claude-haiku-4-5-20251001
16
+ const MODEL = process.env.FORGE_MODEL ?? "claude-sonnet-4-6";
17
+ const FALLBACK_MODEL = process.env.FORGE_FALLBACK_MODEL ?? "claude-haiku-4-5-20251001";
18
+ const CLAUDE_BIN = process.env.CLAUDE_BIN ?? "claude";
19
+ // ─── Internal helpers ─────────────────────────────────────────────────────────
20
+ function extractJSON(text) {
21
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
22
+ if (fenced)
23
+ return JSON.parse(fenced[1].trim());
24
+ const start = text.indexOf("{");
25
+ const end = text.lastIndexOf("}");
26
+ if (start !== -1 && end !== -1)
27
+ return JSON.parse(text.slice(start, end + 1));
28
+ throw new Error("No JSON found in response");
29
+ }
30
+ function shortenPath(p, cwd) {
31
+ if (!p || typeof p !== "string")
32
+ return p;
33
+ if (cwd && p.startsWith(cwd + "/"))
34
+ return p.slice(cwd.length + 1);
35
+ if (cwd && p === cwd)
36
+ return ".";
37
+ return p;
38
+ }
39
+ /**
40
+ * Translate a stream-json `tool_use` block into a user-friendly one-line log.
41
+ * Returns null for tools we don't want to surface (filters out noise).
42
+ */
43
+ function formatToolUse(block, cwd) {
44
+ const name = block?.name;
45
+ const input = block?.input ?? {};
46
+ switch (name) {
47
+ case "Read": {
48
+ const path = shortenPath(input.file_path, cwd);
49
+ return path ? `Reading ${path}` : null;
50
+ }
51
+ case "Glob": {
52
+ if (!input.pattern)
53
+ return null;
54
+ const where = input.path ? ` in ${shortenPath(input.path, cwd)}` : "";
55
+ return `Globbing ${input.pattern}${where}`;
56
+ }
57
+ case "Grep": {
58
+ if (!input.pattern)
59
+ return null;
60
+ const where = input.path ? ` in ${shortenPath(input.path, cwd)}` : "";
61
+ return `Grepping ${JSON.stringify(input.pattern)}${where}`;
62
+ }
63
+ default:
64
+ return null;
65
+ }
66
+ }
67
+ async function runClaude(prompt, jsonSchema, systemPrompt, model, options = {}) {
68
+ const { cwd, allowedTools, onEvent, sessionId, resumeSession, signal, timeout } = options;
69
+ if (signal?.aborted) {
70
+ const err = new Error("Claude CLI call cancelled before start.");
71
+ err.name = "AbortError";
72
+ throw err;
73
+ }
74
+ const streaming = typeof onEvent === "function";
75
+ const args = [
76
+ "--print",
77
+ "--output-format", streaming ? "stream-json" : "json",
78
+ ...(streaming ? ["--verbose"] : []),
79
+ "--model", model ?? MODEL,
80
+ "--fallback-model", FALLBACK_MODEL,
81
+ // Kill every tool by default: without this, Claude sometimes picks up
82
+ // TodoWrite/Task/WebSearch and spends turns on them, returning
83
+ // stop_reason=end_turn with an empty result. When the caller explicitly
84
+ // passes allowedTools (e.g. refine with repo access), allow only those.
85
+ // NOTE: we can't use `--bare` — it disables keychain/OAuth reads, which
86
+ // breaks the Claude Code subscription auth path.
87
+ ...(allowedTools?.length
88
+ ? ["--allowed-tools", allowedTools.join(",")]
89
+ : ["--tools", ""]),
90
+ // Skip user/project/local settings (hooks, agents, CLAUDE.md cascade,
91
+ // auto-mode, memory). Loading those was causing extra turns that came
92
+ // back with stop_reason=end_turn and result="". Verified: same prompt
93
+ // goes from failing at turns=2/3 → passing at turns=1 with this flag.
94
+ "--setting-sources", "",
95
+ ];
96
+ // Session continuity for multi-turn refine:
97
+ // resumeSession=true → --resume <uuid> (replays the conversation so Claude
98
+ // keeps its prior reasoning, tool-call history, and prompt cache).
99
+ // sessionId without resume → --session-id <uuid> (first turn; fixes the
100
+ // UUID so subsequent turns can resume by the id we own).
101
+ if (resumeSession && sessionId) {
102
+ args.push("--resume", sessionId);
103
+ }
104
+ else if (sessionId) {
105
+ args.push("--session-id", sessionId);
106
+ }
107
+ // NOTE: we deliberately do NOT pass --json-schema anymore. The CLI's schema
108
+ // validator rejects the model's first turn and retries on a second turn; if
109
+ // that also fails, we get stop_reason=end_turn with result="". The prompts
110
+ // already tell Claude to emit raw JSON and `extractJSON` is lenient enough
111
+ // to unwrap a stray fence or preamble. We validate shape on our side instead.
112
+ void jsonSchema;
113
+ // On resume, the system prompt is baked into the session from the first
114
+ // turn — re-passing it would stack a second system message.
115
+ if (systemPrompt && !resumeSession)
116
+ args.push("--system-prompt", systemPrompt);
117
+ return new Promise((resolve, reject) => {
118
+ // Strip ANTHROPIC_API_KEY so the CLI uses the OAuth session
119
+ // (Claude Code subscription) rather than falling back to API credits.
120
+ const { ANTHROPIC_API_KEY: _removed, ...cleanEnv } = process.env;
121
+ const proc = spawn(CLAUDE_BIN, args, {
122
+ env: { ...cleanEnv, FORCE_COLOR: "0" },
123
+ stdio: ["pipe", "pipe", "pipe"],
124
+ ...(cwd ? { cwd } : {}),
125
+ });
126
+ let stdout = "";
127
+ let stderr = "";
128
+ let streamBuffer = "";
129
+ let streamedResult = null;
130
+ const timeoutMs = timeout ?? 600_000;
131
+ const timeoutMin = Math.round(timeoutMs / 60_000);
132
+ const timer = setTimeout(() => {
133
+ proc.kill();
134
+ reject(new Error(`claude CLI timed out after ${timeoutMin} minutes`));
135
+ }, timeoutMs);
136
+ // External abort (e.g. user cancelled the analysis). Kill the subprocess
137
+ // so we stop billing the subscription / token window for work the user
138
+ // has already discarded.
139
+ const onAbort = () => {
140
+ try {
141
+ proc.kill();
142
+ }
143
+ catch { /* ignore */ }
144
+ clearTimeout(timer);
145
+ const err = new Error("Claude CLI call cancelled.");
146
+ err.name = "AbortError";
147
+ reject(err);
148
+ };
149
+ signal?.addEventListener("abort", onAbort, { once: true });
150
+ proc.on("close", () => { signal?.removeEventListener("abort", onAbort); });
151
+ proc.stdout.on("data", (d) => {
152
+ stdout += d;
153
+ if (!streaming)
154
+ return;
155
+ streamBuffer += d.toString("utf8");
156
+ let nl;
157
+ while ((nl = streamBuffer.indexOf("\n")) !== -1) {
158
+ const line = streamBuffer.slice(0, nl).trim();
159
+ streamBuffer = streamBuffer.slice(nl + 1);
160
+ if (!line)
161
+ continue;
162
+ let evt;
163
+ try {
164
+ evt = JSON.parse(line);
165
+ }
166
+ catch {
167
+ continue;
168
+ }
169
+ if (evt.type === "assistant") {
170
+ const blocks = evt.message?.content ?? [];
171
+ for (const block of blocks) {
172
+ if (block.type !== "tool_use")
173
+ continue;
174
+ const msg = formatToolUse(block, cwd);
175
+ if (msg) {
176
+ try {
177
+ onEvent({ type: "log", message: msg });
178
+ }
179
+ catch { /* swallow */ }
180
+ }
181
+ }
182
+ }
183
+ else if (evt.type === "result") {
184
+ streamedResult = evt;
185
+ }
186
+ }
187
+ });
188
+ proc.stderr.on("data", (d) => { stderr += d; });
189
+ proc.on("close", (code) => {
190
+ clearTimeout(timer);
191
+ if (code !== 0) {
192
+ try {
193
+ const wrapper = streamedResult ?? JSON.parse(stdout.trim());
194
+ const msg = wrapper.result || wrapper.error || stderr.trim() || stdout.trim();
195
+ reject(new Error(`claude CLI error: ${msg}`));
196
+ }
197
+ catch {
198
+ reject(new Error(`claude CLI error: ${stderr.trim() || stdout.trim()}`));
199
+ }
200
+ return;
201
+ }
202
+ let wrapper;
203
+ if (streaming) {
204
+ if (!streamedResult) {
205
+ reject(new Error(`claude CLI stream ended without a result event (${stdout.length} bytes). First 200 chars: ${stdout.slice(0, 200)}`));
206
+ return;
207
+ }
208
+ wrapper = streamedResult;
209
+ }
210
+ else {
211
+ try {
212
+ wrapper = JSON.parse(stdout.trim());
213
+ }
214
+ catch {
215
+ reject(new Error(`claude CLI returned non-JSON output (${stdout.length} bytes). First 200 chars: ${stdout.slice(0, 200)}`));
216
+ return;
217
+ }
218
+ }
219
+ const result = wrapper.result ?? "";
220
+ if (!result.trim()) {
221
+ // Claude succeeded but emitted no content. Usually means the model
222
+ // got stuck in a tool-use loop or was blocked by --disallowed-tools.
223
+ // Surface a clean, actionable message instead of dumping the wrapper.
224
+ const stopReason = wrapper.stop_reason ?? "unknown";
225
+ const turns = wrapper.num_turns ?? 0;
226
+ const hint = stopReason === "tool_use"
227
+ ? "model tried to use a blocked tool — retry or allow the tool."
228
+ : "model produced empty output — retry or switch model.";
229
+ reject(new Error(`Claude returned no content (stop_reason: ${stopReason}, turns: ${turns}). ${hint}`));
230
+ return;
231
+ }
232
+ let data;
233
+ try {
234
+ data = extractJSON(result);
235
+ }
236
+ catch (err) {
237
+ const msg = err instanceof Error ? err.message : String(err);
238
+ reject(new Error(`Claude response was not valid JSON — ${msg}. First 200 chars of result: ${result.slice(0, 200)}`));
239
+ return;
240
+ }
241
+ // The CLI echoes the session id it actually used. If it doesn't match
242
+ // the one we passed (CLI version drift, resume silently forked to a
243
+ // new session, etc.), warn — the caller should persist the CLI's id
244
+ // going forward or we'll resume the wrong conversation next turn.
245
+ const wrapperSessionId = wrapper.session_id ?? null;
246
+ if (sessionId && wrapperSessionId && wrapperSessionId !== sessionId) {
247
+ console.warn(`[claudeClient] session_id mismatch: passed ${sessionId}, CLI returned ${wrapperSessionId}. Using CLI value.`);
248
+ }
249
+ resolve({
250
+ data,
251
+ cost: wrapper.total_cost_usd ?? 0,
252
+ serviceTier: wrapper.usage?.service_tier ?? "unknown",
253
+ inputTokens: wrapper.usage?.input_tokens ?? 0,
254
+ outputTokens: wrapper.usage?.output_tokens ?? 0,
255
+ cacheCreationTokens: wrapper.usage?.cache_creation_input_tokens ?? 0,
256
+ cacheReadTokens: wrapper.usage?.cache_read_input_tokens ?? 0,
257
+ sessionId: wrapperSessionId ?? sessionId ?? null,
258
+ });
259
+ });
260
+ proc.stdin.write(prompt, "utf8");
261
+ proc.stdin.end();
262
+ });
263
+ }
264
+ // ─── Public API ───────────────────────────────────────────────────────────────
265
+ export async function checkResolution(issue, diffs, model, opts = {}) {
266
+ const { data, cost, serviceTier, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens } = await runClaude(buildCheckResolutionPrompt(issue, diffs), RESOLUTION_CHECK_SCHEMA, "You are Forge, a code review assistant. Assess whether code changes actually fix the reported issue. Be concise and direct.", model, { signal: opts.signal, timeout: opts.timeout });
267
+ return { ...data, cost, serviceTier, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens };
268
+ }
269
+ export async function identifyRelevantFiles(issue, fileTree, model, opts = {}) {
270
+ const { data, cost, serviceTier, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens } = await runClaude(buildIdentifyFilesPrompt(issue, fileTree), SELECT_FILES_SCHEMA, "You are Forge, a code review assistant. You only output raw JSON — never markdown or prose.", model, { signal: opts.signal, timeout: opts.timeout });
271
+ return { ...data, cost, serviceTier, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens };
272
+ }
273
+ export async function refineOrAsk(issue, fileContents, options = {}, model, onEvent, opts = {}) {
274
+ const { repoCwd, sessionId, resumeSession } = options;
275
+ const hasRepoAccess = Boolean(repoCwd);
276
+ const accessLine = hasRepoAccess
277
+ ? `You ARE running inside a checkout of the repo at the current working directory, with the user's proposed changes already overlaid on the working tree. You can use Read, Glob, and Grep to inspect any file in the repo. Use them when you need context beyond the files listed in the prompt.`
278
+ : `The prompt lists the file paths in the repo and the full content of the files currently in scope. Treat those as authoritative — do NOT claim you lack repo access. If you need a file that isn't attached, name the path and ask the user to share it.`;
279
+ // When resuming, the system prompt + issue/files/previousChanges are already
280
+ // in Claude's context from the initial turn — send only what's new.
281
+ const prompt = resumeSession
282
+ ? buildRefineResumePrompt(options)
283
+ : buildRefineOrAskPrompt(issue, fileContents, options);
284
+ const runOptions = hasRepoAccess
285
+ ? { cwd: repoCwd, allowedTools: ["Read", "Glob", "Grep"], onEvent }
286
+ : { onEvent };
287
+ if (sessionId)
288
+ runOptions.sessionId = sessionId;
289
+ if (resumeSession)
290
+ runOptions.resumeSession = true;
291
+ if (opts.signal)
292
+ runOptions.signal = opts.signal;
293
+ if (opts.timeout)
294
+ runOptions.timeout = opts.timeout;
295
+ const { data, cost, serviceTier, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, sessionId: returnedSessionId } = await runClaude(prompt, REFINE_OR_ASK_SCHEMA, `You are Forge, an AI code assistant in a production React/TypeScript/Express codebase.
296
+ ${accessLine}
297
+ You auto-classify the user's message: answer questions about state, or produce a refined code change when given a directive.
298
+ Rules when producing changes:
299
+ - Changes must be minimal and surgical.
300
+ - Match the existing code style exactly (TypeScript strict, no barrel imports, Tailwind for UI).
301
+ - For "proposed", always return the COMPLETE file content.
302
+ - Maximum 5 files changed.
303
+ - Never add console.* — use the existing log utility.
304
+ - Never add comments unless the logic is non-obvious.
305
+ Rules when answering:
306
+ - Ground statements in the validation state, the proposed changes, and (when available) files you read from the working tree.
307
+ - If validation hasn't been run yet, say so plainly.
308
+ - Be concise — one to four short paragraphs at most.
309
+ Your final message MUST be a single raw JSON object matching the schema — no prose, no code fences, no preamble.`, model, runOptions);
310
+ return { ...data, cost, serviceTier, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, sessionId: returnedSessionId };
311
+ }
312
+ export async function proposeFix(issue, fileContents, options = {}, model, opts = {}) {
313
+ const schema = options.ciContext ? CI_FIX_SCHEMA : PROPOSE_FIX_SCHEMA;
314
+ const kind = classifyIssue(issue);
315
+ const isFeature = kind === "feature" || kind === "refactor";
316
+ const systemPrompt = isFeature
317
+ ? `You are Forge, an AI code assistant in a production React/TypeScript/Express codebase.
318
+ Rules:
319
+ - Match the existing code style exactly (TypeScript strict, no barrel imports, Tailwind for UI).
320
+ - For "proposed", always return the COMPLETE file content, not a diff or snippet.
321
+ - Maximum 12 files changed. Prioritise core changes over secondary ones — if the feature touches more files than the cap, implement the most important changes and note the rest in "explanation".
322
+ - Never add console.* — use the existing log utility.
323
+ - Never add comments unless the logic is non-obvious.
324
+ - Before returning, verify TypeScript correctness: no implicit any, no missing imports, no type mismatches, all imported symbols must actually be exported from their source.
325
+ Feature discipline:
326
+ - Follow the issue specification. Only make changes the issue asks for — do not clean up, refactor, or "improve" unrelated code.
327
+ - When the issue says to ADD an entry to an existing list or config (nav items, routes, type unions), add the entry — do not restructure the existing code around it.
328
+ - Base new files on the patterns and conventions visible in the relevant files.
329
+ - Use a "feat:" conventional-commit prefix in the title, not "fix:".`
330
+ : `You are Forge, an AI code assistant in a production React/TypeScript/Express codebase.
331
+ Rules:
332
+ - Changes must be minimal and surgical.
333
+ - Match the existing code style exactly (TypeScript strict, no barrel imports, Tailwind for UI).
334
+ - For "proposed", always return the COMPLETE file content, not a diff or snippet.
335
+ - Maximum 5 files changed.
336
+ - Never add console.* — use the existing log utility.
337
+ - Never add comments unless the logic is non-obvious.
338
+ - Before returning, verify TypeScript correctness: no implicit any, no missing imports, no type mismatches, all imported symbols must actually be exported from their source.
339
+ - If fixing a CI failure, address only what the logs report — do not change unrelated code.
340
+ Root-cause discipline:
341
+ - A fix that only improves an error message or adds a guard at the point of failure is a symptom suppressor, not a fix. Reject it and find the upstream origin of the bad input.
342
+ - When a value is undefined/null at a check, trace one or two frames up (the caller of the function, the prop passed into the component, the handler bound to an onClick). Default function parameters are defeated when the caller passes an unexpected argument (e.g. React forwarding a MouseEvent) — look for that class of bug before patching the consumer.
343
+ - In "explanation", name the origin of the bad value and state why the reported symptom can no longer occur after your change.`;
344
+ const { data, cost, serviceTier, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens } = await runClaude(buildProposeFixPrompt(issue, fileContents, options), schema, systemPrompt, model, { signal: opts.signal, timeout: opts.timeout });
345
+ return { ...data, cost, serviceTier, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens };
346
+ }
347
+ //# sourceMappingURL=claudeClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claudeClient.js","sourceRoot":"","sources":["../../../../server/forge/core/claudeClient.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Forge persistent config.
3
+ *
4
+ * Storage contract
5
+ * ────────────────
6
+ * A single JSON blob on disk (default: ~/.forge/config.json). Loaded once at
7
+ * process start into an in-memory cache so route handlers read synchronously.
8
+ * Writes are atomic (tmp-file + rename) and serialized to avoid interleaving.
9
+ *
10
+ * Swap to Redis or another backend by re-exporting a different instance from
11
+ * this module — callers import `get()` / `patch()` and don't know the source.
12
+ */
13
+ import type { ForgeConfig } from "./types.ts";
14
+ type ProviderKey = "gitlab" | "github" | "azuredevops";
15
+ /** Returns the current cached config. Synchronous. */
16
+ export declare function get(): ForgeConfig;
17
+ /**
18
+ * Deep-merges `patch` into the current config, persists, and updates the
19
+ * cache. Arrays are replaced (not merged) so callers send the whole repos
20
+ * list when editing it.
21
+ */
22
+ export declare function patch(delta: Record<string, unknown>): Promise<void>;
23
+ /** Escape hatch for tests / explicit reload from disk. */
24
+ export declare function reload(): ForgeConfig;
25
+ /**
26
+ * Resolves the primary project identifier.
27
+ * Priority: persisted config repos[0] → FORGE_REPOS first entry → FORGE_GITLAB_PROJECT.
28
+ */
29
+ export declare function getPrimaryProject(): string;
30
+ /**
31
+ * Resolved provider connection: config value wins, env var fills gaps, builtin
32
+ * URL is the last fallback. Providers call this instead of reading process.env.
33
+ *
34
+ * @param {"gitlab"|"github"|"azuredevops"} provider
35
+ * @returns {{ baseUrl?: string, token: string }}
36
+ */
37
+ export declare function getConnection(provider: ProviderKey): {
38
+ token: string;
39
+ baseUrl?: string;
40
+ };
41
+ /**
42
+ * Safe view for the API — raw tokens are replaced with a short hint and a
43
+ * boolean so the UI can render state without ever seeing a secret.
44
+ */
45
+ export declare function getPublic(): {
46
+ repos: import("./types.ts").ForgeRepo[];
47
+ defaults: {
48
+ model: string | null;
49
+ agent: string | null;
50
+ autoPushOnValidatePass: boolean;
51
+ refineRepoAccess: boolean;
52
+ promptRules: string;
53
+ testDeployUrlTemplate?: string;
54
+ cliTimeout?: number;
55
+ };
56
+ connections: {
57
+ gitlab: {
58
+ baseUrl: string;
59
+ hasToken: boolean;
60
+ tokenHint: string;
61
+ tokenSource: string;
62
+ };
63
+ github: {
64
+ hasToken: boolean;
65
+ tokenHint: string;
66
+ tokenSource: string;
67
+ };
68
+ azuredevops: {
69
+ hasToken: boolean;
70
+ tokenHint: string;
71
+ tokenSource: string;
72
+ };
73
+ };
74
+ };
75
+ export {};
76
+ //# sourceMappingURL=configStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configStore.d.ts","sourceRoot":"","sources":["../../../../server/forge/core/configStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,KAAK,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,aAAa,CAAC;AAqHvD,sDAAsD;AACtD,wBAAgB,GAAG,gBAElB;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAOnE;AAED,0DAA0D;AAC1D,wBAAgB,MAAM,gBAGrB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,WAKhC;AAQD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,WAAW;;;EAQlD;AAQD;;;GAGG;AACH,wBAAgB,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyBxB"}
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Forge persistent config.
3
+ *
4
+ * Storage contract
5
+ * ────────────────
6
+ * A single JSON blob on disk (default: ~/.forge/config.json). Loaded once at
7
+ * process start into an in-memory cache so route handlers read synchronously.
8
+ * Writes are atomic (tmp-file + rename) and serialized to avoid interleaving.
9
+ *
10
+ * Swap to Redis or another backend by re-exporting a different instance from
11
+ * this module — callers import `get()` / `patch()` and don't know the source.
12
+ */
13
+ import fsSync from "node:fs";
14
+ import fs from "node:fs/promises";
15
+ import os from "node:os";
16
+ import path from "node:path";
17
+ /** @typedef {{ id: string, label: string, projectType?: "auto" | "node" | "go" }} ForgeRepo */
18
+ /**
19
+ * @typedef {{ baseUrl: string, token: string }} GitLabConnection
20
+ * @typedef {{ token: string }} GitHubConnection
21
+ * @typedef {{ token: string }} AzureDevOpsConnection
22
+ *
23
+ * @typedef {{
24
+ * repos: ForgeRepo[],
25
+ * defaults: {
26
+ * model: string | null,
27
+ * agent: string | null,
28
+ * autoPushOnValidatePass: boolean,
29
+ * refineRepoAccess: boolean,
30
+ * promptRules: string,
31
+ * },
32
+ * connections: {
33
+ * gitlab: GitLabConnection,
34
+ * github: GitHubConnection,
35
+ * azuredevops: AzureDevOpsConnection,
36
+ * }
37
+ * }} ForgeConfig
38
+ */
39
+ const BUILTIN_BASE_URLS = {
40
+ gitlab: "https://gitlab.com/api/v4",
41
+ github: "https://api.github.com",
42
+ };
43
+ const DEFAULT_CONFIG = {
44
+ repos: [],
45
+ defaults: {
46
+ model: null,
47
+ agent: null,
48
+ autoPushOnValidatePass: false,
49
+ // When true (default), Refine gets Read/Glob/Grep access to a local
50
+ // clone of the primary repo. Off → lighter "file tree in prompt" flow.
51
+ // The FORGE_REFINE_REPO_ACCESS env var, when set, overrides this.
52
+ refineRepoAccess: true,
53
+ // Free-form rules injected into every analysis and refine prompt.
54
+ // Lets the user steer the model without touching code.
55
+ promptRules: "",
56
+ // Test-deploy URL surfaced next to "Approve & Create MR" when the
57
+ // branch pipeline is green. Plain URL (e.g. "https://test.example.com")
58
+ // for a shared env; supports "{branch}" / "{slug}" placeholders when
59
+ // moving to ephemeral per-branch deploys.
60
+ testDeployUrlTemplate: "",
61
+ },
62
+ connections: {
63
+ gitlab: { baseUrl: BUILTIN_BASE_URLS.gitlab, token: "" },
64
+ github: { token: "", owner: "", repo: "", baseBranch: "main" },
65
+ azuredevops: { token: "", org: "", project: "", repo: "", baseBranch: "main" },
66
+ },
67
+ };
68
+ function configPath() {
69
+ return process.env.FORGE_CONFIG_PATH ?? path.join(os.homedir(), ".forge", "config.json");
70
+ }
71
+ /**
72
+ * Synchronous load — called once at startup so the rest of the routes can
73
+ * read config without awaiting. If the file is missing we return defaults;
74
+ * if it's malformed we warn and return defaults (don't crash the server over
75
+ * a bad config).
76
+ */
77
+ function loadSync() {
78
+ const p = configPath();
79
+ try {
80
+ const raw = fsSync.readFileSync(p, "utf8");
81
+ const parsed = JSON.parse(raw);
82
+ return mergeDeep(DEFAULT_CONFIG, parsed);
83
+ }
84
+ catch (err) {
85
+ const e = err;
86
+ if (e.code === "ENOENT")
87
+ return structuredClone(DEFAULT_CONFIG);
88
+ console.warn(`[forge] config file at ${p} is unreadable (${e.message}); using defaults`);
89
+ return structuredClone(DEFAULT_CONFIG);
90
+ }
91
+ }
92
+ let cache = loadSync();
93
+ /** Serializes writes so two concurrent PUTs can't interleave. */
94
+ let inFlight = Promise.resolve();
95
+ async function writeToDisk(cfg) {
96
+ const p = configPath();
97
+ await fs.mkdir(path.dirname(p), { recursive: true });
98
+ const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
99
+ await fs.writeFile(tmp, JSON.stringify(cfg, null, 2), "utf8");
100
+ // 0600 — owner read/write only. Belt-and-suspenders for when tokens land here.
101
+ await fs.chmod(tmp, 0o600);
102
+ await fs.rename(tmp, p);
103
+ }
104
+ function mergeDeep(base, patch) {
105
+ const out = { ...base };
106
+ for (const [k, v] of Object.entries(patch)) {
107
+ const bv = base[k];
108
+ if (v !== null &&
109
+ typeof v === "object" &&
110
+ !Array.isArray(v) &&
111
+ bv !== null &&
112
+ typeof bv === "object" &&
113
+ !Array.isArray(bv)) {
114
+ out[k] = mergeDeep(bv, v);
115
+ }
116
+ else {
117
+ out[k] = v;
118
+ }
119
+ }
120
+ return out;
121
+ }
122
+ /** Returns the current cached config. Synchronous. */
123
+ export function get() {
124
+ return cache;
125
+ }
126
+ /**
127
+ * Deep-merges `patch` into the current config, persists, and updates the
128
+ * cache. Arrays are replaced (not merged) so callers send the whole repos
129
+ * list when editing it.
130
+ */
131
+ export function patch(delta) {
132
+ inFlight = inFlight.then(async () => {
133
+ const next = mergeDeep(cache, delta);
134
+ await writeToDisk(next);
135
+ cache = next;
136
+ });
137
+ return inFlight;
138
+ }
139
+ /** Escape hatch for tests / explicit reload from disk. */
140
+ export function reload() {
141
+ cache = loadSync();
142
+ return cache;
143
+ }
144
+ /**
145
+ * Resolves the primary project identifier.
146
+ * Priority: persisted config repos[0] → FORGE_REPOS first entry → FORGE_GITLAB_PROJECT.
147
+ */
148
+ export function getPrimaryProject() {
149
+ const repos = cache.repos;
150
+ if (repos.length > 0)
151
+ return repos[0].id;
152
+ const raw = process.env.FORGE_REPOS ?? process.env.FORGE_GITLAB_PROJECT ?? "";
153
+ return raw.split(",")[0]?.trim() || null;
154
+ }
155
+ const ENV_TOKEN_KEYS = {
156
+ gitlab: "GITLAB_TOKEN",
157
+ github: "GITHUB_TOKEN",
158
+ azuredevops: "AZURE_DEVOPS_TOKEN",
159
+ };
160
+ /**
161
+ * Resolved provider connection: config value wins, env var fills gaps, builtin
162
+ * URL is the last fallback. Providers call this instead of reading process.env.
163
+ *
164
+ * @param {"gitlab"|"github"|"azuredevops"} provider
165
+ * @returns {{ baseUrl?: string, token: string }}
166
+ */
167
+ export function getConnection(provider) {
168
+ const fromCfg = cache.connections?.[provider] ?? {};
169
+ const envToken = process.env[ENV_TOKEN_KEYS[provider]] ?? "";
170
+ const baseUrl = fromCfg.baseUrl || BUILTIN_BASE_URLS[provider];
171
+ return {
172
+ ...(baseUrl ? { baseUrl } : {}),
173
+ token: fromCfg.token || envToken,
174
+ };
175
+ }
176
+ function maskToken(t) {
177
+ if (!t)
178
+ return "";
179
+ if (t.length <= 4)
180
+ return "••••";
181
+ return `••••${t.slice(-4)}`;
182
+ }
183
+ /**
184
+ * Safe view for the API — raw tokens are replaced with a short hint and a
185
+ * boolean so the UI can render state without ever seeing a secret.
186
+ */
187
+ export function getPublic() {
188
+ const cfg = cache;
189
+ const conn = cfg.connections ?? DEFAULT_CONFIG.connections;
190
+ return {
191
+ repos: cfg.repos,
192
+ defaults: cfg.defaults,
193
+ connections: {
194
+ gitlab: {
195
+ baseUrl: conn.gitlab?.baseUrl ?? BUILTIN_BASE_URLS.gitlab,
196
+ hasToken: Boolean(conn.gitlab?.token || process.env.GITLAB_TOKEN),
197
+ tokenHint: maskToken(conn.gitlab?.token || process.env.GITLAB_TOKEN || ""),
198
+ tokenSource: conn.gitlab?.token ? "config" : process.env.GITLAB_TOKEN ? "env" : "none",
199
+ },
200
+ github: {
201
+ hasToken: Boolean(conn.github?.token || process.env.GITHUB_TOKEN),
202
+ tokenHint: maskToken(conn.github?.token || process.env.GITHUB_TOKEN || ""),
203
+ tokenSource: conn.github?.token ? "config" : process.env.GITHUB_TOKEN ? "env" : "none",
204
+ },
205
+ azuredevops: {
206
+ hasToken: Boolean(conn.azuredevops?.token || process.env.AZURE_DEVOPS_TOKEN),
207
+ tokenHint: maskToken(conn.azuredevops?.token || process.env.AZURE_DEVOPS_TOKEN || ""),
208
+ tokenSource: conn.azuredevops?.token ? "config" : process.env.AZURE_DEVOPS_TOKEN ? "env" : "none",
209
+ },
210
+ },
211
+ };
212
+ }
213
+ //# sourceMappingURL=configStore.js.map