catalyst-core 0.1.0-canary.9 → 0.2.0-beta.2

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 (148) hide show
  1. package/README.md +1 -1
  2. package/bin/catalyst.js +8 -1
  3. package/changelog.md +6 -0
  4. package/dist/native/androidProject/app/build.gradle.kts +34 -8
  5. package/dist/native/androidProject/app/src/main/AndroidManifest.xml +2 -1
  6. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +1 -1
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +270 -78
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +34 -5
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +149 -1
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeCameraManager.kt +337 -0
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/OfflineCacheService.kt +374 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/TransitionManager.kt +202 -0
  13. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +156 -39
  14. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/BarcodeDetector.kt +123 -0
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/CameraSessionManager.kt +244 -0
  16. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/HoldController.kt +70 -0
  17. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/TorchController.kt +57 -0
  18. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamState.kt +41 -0
  19. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamStateMachine.kt +40 -0
  20. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ViewfinderMapper.kt +75 -0
  21. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ZoomController.kt +156 -0
  22. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +7 -0
  23. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +6 -0
  24. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +253 -0
  25. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +16 -5
  26. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +42 -5
  27. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +7 -3
  28. package/dist/native/androidProject/app/src/main/res/drawable/debug_viewfinder_border.xml +8 -0
  29. package/dist/native/androidProject/app/src/main/res/layout/activity_main.xml +37 -0
  30. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +7 -2
  31. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +139 -0
  32. package/dist/native/androidSetup.js +1 -0
  33. package/dist/native/bridge/WebBridge.js +5 -5
  34. package/dist/native/bridge/constants/NativeInterfaces.js +9 -5
  35. package/dist/native/bridge/hooks/index.js +1 -0
  36. package/dist/native/bridge/hooks/useCamera.js +9 -0
  37. package/dist/native/bridge/hooks/useDataProtection.js +2 -0
  38. package/dist/native/bridge/hooks/useDeviceInfo.js +2 -0
  39. package/dist/native/bridge/hooks/useFilePicker.js +2 -0
  40. package/dist/native/bridge/hooks/useGoogleSignIn.js +1 -0
  41. package/dist/native/bridge/hooks/useHapticFeedback.js +1 -0
  42. package/dist/native/bridge/hooks/useIntent.js +1 -0
  43. package/dist/native/bridge/hooks/useNativeTransition.js +43 -0
  44. package/dist/native/bridge/hooks/useNetworkStatus.js +3 -0
  45. package/dist/native/bridge/hooks/useNotification.js +2 -0
  46. package/dist/native/bridge/hooks/useSafeArea.js +2 -0
  47. package/dist/native/bridge/hooks/useVideoStream.js +6 -0
  48. package/dist/native/bridge/hooks/utils.js +1 -0
  49. package/dist/native/bridge/hooks.js +1 -137
  50. package/dist/native/bridge/useBaseHook.js +12 -7
  51. package/dist/native/bridge/utils/NativeBridge.js +41 -6
  52. package/dist/native/buildAndroid/assets.js +52 -0
  53. package/dist/native/buildAndroid/build.js +19 -0
  54. package/dist/native/buildAndroid/config.js +1 -0
  55. package/dist/native/buildAndroid/index.js +1 -0
  56. package/dist/native/buildAppAndroid.js +2 -150
  57. package/dist/native/buildAppIos.js +2 -312
  58. package/dist/native/buildIos/assets.js +8 -0
  59. package/dist/native/buildIos/build.js +40 -0
  60. package/dist/native/buildIos/config.js +71 -0
  61. package/dist/native/buildIos/index.js +6 -0
  62. package/dist/native/buildIos/plugins.js +45 -0
  63. package/dist/native/internalPluginUtils.js +7 -0
  64. package/dist/native/iosnativeWebView/Package.resolved +36 -0
  65. package/dist/native/iosnativeWebView/Sources/Core/Camera/BarcodeDetector.swift +116 -0
  66. package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraPreviewView.swift +58 -0
  67. package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraSessionManager.swift +308 -0
  68. package/dist/native/iosnativeWebView/Sources/Core/Camera/HoldController.swift +63 -0
  69. package/dist/native/iosnativeWebView/Sources/Core/Camera/NativeCameraManager.swift +194 -0
  70. package/dist/native/iosnativeWebView/Sources/Core/Camera/TorchController.swift +60 -0
  71. package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamState.swift +44 -0
  72. package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamStateMachine.swift +60 -0
  73. package/dist/native/iosnativeWebView/Sources/Core/Camera/ZoomController.swift +130 -0
  74. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +11 -1
  75. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +2 -1
  76. package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +5 -0
  77. package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +6 -0
  78. package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +364 -0
  79. package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +39 -4
  80. package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineCacheService.swift +683 -0
  81. package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineURLSchemeHandler.swift +94 -0
  82. package/dist/native/iosnativeWebView/Sources/Core/Utils/ResourceURLProtocol.swift +19 -6
  83. package/dist/native/iosnativeWebView/Sources/Core/Utils/SafeAreaUtils.swift +1 -0
  84. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +1 -0
  85. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +4 -1
  86. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +227 -4
  87. package/dist/native/iosnativeWebView/Sources/Core/WebView/TransitionManager.swift +169 -0
  88. package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +14 -0
  89. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +53 -8
  90. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +88 -4
  91. package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +30 -11
  92. package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +11 -17
  93. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +8 -2
  94. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +1 -1
  95. package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +4 -2
  96. package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +160 -0
  97. package/dist/native/plugin-bridge/PluginBridge.js +1 -0
  98. package/dist/native/pluginComposerAndroid.js +28 -0
  99. package/dist/native/pluginComposerIos.js +16 -0
  100. package/dist/native/renameAndroidProject.js +5 -5
  101. package/dist/native/setupEmulatorIos.js +6 -4
  102. package/dist/native/setupServer.js +2 -3
  103. package/dist/native/utils.js +4 -1
  104. package/dist/offline/catalyst-sw.js +1 -0
  105. package/dist/offline/registerServiceWorker.js +1 -0
  106. package/dist/otel.js +70 -2
  107. package/dist/router/ClientRouter.js +1 -1
  108. package/dist/scripts/build.js +1 -1
  109. package/dist/scripts/cleanCache.js +1 -0
  110. package/dist/scripts/devBuild.js +1 -1
  111. package/dist/scripts/devServe.js +1 -1
  112. package/dist/scripts/generateOfflineManifest.js +1 -0
  113. package/dist/scripts/loadEnvironmentVariables.js +1 -2
  114. package/dist/scripts/plugins.js +1 -0
  115. package/dist/scripts/registerAliases.js +6 -2
  116. package/dist/scripts/scriptUtils.js +2 -1
  117. package/dist/scripts/serve.js +2 -1
  118. package/dist/scripts/start.js +7 -2
  119. package/dist/server/expressServer.js +4 -8
  120. package/dist/server/renderer/document/Body.js +4 -4
  121. package/dist/server/renderer/document/Head.js +2 -2
  122. package/dist/server/renderer/extract.js +31 -27
  123. package/dist/server/renderer/handler.js +47 -24
  124. package/dist/server/renderer/index.js +1 -1
  125. package/dist/server/startServer.js +57 -7
  126. package/dist/server/utils/userAgentUtil.js +9 -3
  127. package/dist/server/utils/validator.js +1 -1
  128. package/dist/webpack/babel-plugins/remove-client.plugin.js +2 -2
  129. package/dist/webpack/babel-plugins/remove-ssr.plugin.js +2 -2
  130. package/dist/webpack/babel.config.client.js +1 -1
  131. package/dist/webpack/babel.config.ssr.js +1 -1
  132. package/dist/webpack/base.babel.js +7 -8
  133. package/dist/webpack/development.client.babel.js +7 -16
  134. package/dist/webpack/production.client.babel.js +1 -1
  135. package/dist/webpack/production.ssr.babel.js +5 -1
  136. package/mcp_v2/knowledge-base.json +206 -100
  137. package/mcp_v2/lib/helpers.js +42 -8
  138. package/mcp_v2/lib/seed.js +88 -0
  139. package/mcp_v2/mcp.js +150 -4
  140. package/mcp_v2/setup.js +30 -193
  141. package/mcp_v2/tools/build.js +1 -1
  142. package/mcp_v2/tools/config.js +16 -4
  143. package/mcp_v2/tools/conversion.js +11 -11
  144. package/mcp_v2/tools/github.js +1117 -0
  145. package/mcp_v2/tools/knowledge.js +16 -3
  146. package/mcp_v2/tools/sync.js +179 -10
  147. package/mcp_v2/tools/tasks.js +13 -4
  148. package/package.json +20 -8
@@ -0,0 +1,1117 @@
1
+ "use strict"
2
+
3
+ /**
4
+ * tools/github.js - Catalyst MCP v2
5
+ *
6
+ * GitHub issue workflow for AI agents:
7
+ * create_github_issue({ dry_run: true }) - first asks the developer to select labels
8
+ * create_github_issue({ dry_run: true, labels }) - gathers context, templates, and duplicate candidates
9
+ * create_github_issue({ dry_run: false }) - publishes after explicit developer approval
10
+ * markdown fallback is generated internally when auth/API publishing fails
11
+ *
12
+ * Authentication:
13
+ * 1. Direct GitHub API token from GITHUB_TOKEN, GITHUB_PAT, or CATALYST_GITHUB_TOKEN
14
+ * 2. Optional fallback to GitHub CLI session (`gh auth token`)
15
+ */
16
+
17
+ const fs = require("fs")
18
+ const path = require("path")
19
+ const https = require("https")
20
+ const { execSync } = require("child_process")
21
+
22
+ let _projectInfo = null
23
+
24
+ const GITHUB_OWNER = "tata1mg"
25
+ const GITHUB_REPO = "catalyst-core"
26
+ const FALLBACK_DIR = ".mcp_issues"
27
+
28
+ const DEFAULT_LABELS = [
29
+ "bug",
30
+ "dependencies",
31
+ "documentation",
32
+ "duplicate",
33
+ "enhancement",
34
+ "good first issue",
35
+ "help wanted",
36
+ "invalid",
37
+ "question",
38
+ "wontfix",
39
+ ]
40
+
41
+ const LABEL_GUIDANCE = {
42
+ bug: "Something is broken, regressed, ignored, crashes, or behaves differently from the documented/configured behavior.",
43
+ dependencies:
44
+ "Dependency/package update work, package-lock changes, npm audit fixes, or vulnerability remediation.",
45
+ documentation: "Docs, README, examples, guides, migration notes, tutorials, or wording fixes.",
46
+ duplicate: "The same issue already exists.",
47
+ enhancement: "New capability, framework improvement, or actionable feature request.",
48
+ "good first issue": "Small, well-scoped change suitable for newcomers.",
49
+ "help wanted": "Needs extra maintainer/community attention or implementation help.",
50
+ invalid: "The report is not applicable or does not describe a valid Catalyst problem.",
51
+ question: "Needs clarification, usage guidance, or design direction before implementation.",
52
+ wontfix: "A consciously unsupported or not-planned request.",
53
+ }
54
+
55
+ const ISSUE_TEMPLATES = {
56
+ bug: {
57
+ label: "bug",
58
+ name: "Bug report",
59
+ use_when:
60
+ "Something is not working, is ignored, crashes, regresses, or differs from expected Catalyst behavior.",
61
+ sections: [
62
+ "Preflight Checklist",
63
+ "What's Wrong?",
64
+ "Steps to Reproduce",
65
+ "Expected Behavior",
66
+ "Actual Behavior",
67
+ "Error Messages/Logs",
68
+ "Environment",
69
+ "What I Tried",
70
+ "Additional Information",
71
+ "Related Issues",
72
+ ],
73
+ },
74
+ enhancement: {
75
+ label: "enhancement",
76
+ name: "Feature request / enhancement",
77
+ use_when:
78
+ "A new capability, universal hook behavior, CLI improvement, or framework ergonomics improvement is needed.",
79
+ sections: [
80
+ "Summary",
81
+ "Motivation",
82
+ "Proposed Behaviour",
83
+ "Example Usage / Output",
84
+ "Proposed Fix",
85
+ "Optional Follow-ups",
86
+ ],
87
+ },
88
+ documentation: {
89
+ label: "documentation",
90
+ name: "Documentation improvement",
91
+ use_when:
92
+ "Docs, examples, README, guides, migration notes, or API reference need to be added or corrected.",
93
+ sections: [
94
+ "Summary",
95
+ "Current Documentation",
96
+ "Suggested Documentation",
97
+ "Affected Pages / Examples",
98
+ "Optional Follow-ups",
99
+ ],
100
+ },
101
+ dependencies: {
102
+ label: "dependencies",
103
+ name: "Dependency update",
104
+ use_when:
105
+ "A dependency, lockfile, audit finding, package version, or security update needs attention.",
106
+ sections: [
107
+ "Summary",
108
+ "Current Dependency",
109
+ "Target Dependency",
110
+ "Reason / Impact",
111
+ "Validation Plan",
112
+ ],
113
+ },
114
+ question: {
115
+ label: "question",
116
+ name: "Question / clarification",
117
+ use_when:
118
+ "The report primarily asks for usage guidance, clarification, or a decision before implementation.",
119
+ sections: ["Question", "Context", "What I Tried", "Expected Guidance"],
120
+ },
121
+ }
122
+
123
+ function init(projectInfo) {
124
+ _projectInfo = projectInfo
125
+ }
126
+
127
+ function getProjectRoot(args = {}) {
128
+ return args.project_path || (_projectInfo && _projectInfo.dir) || process.cwd()
129
+ }
130
+
131
+ function safeReadJson(filePath) {
132
+ try {
133
+ return JSON.parse(fs.readFileSync(filePath, "utf8"))
134
+ } catch {
135
+ return null
136
+ }
137
+ }
138
+
139
+ function safeReadText(filePath, maxBytes = 8000) {
140
+ let fd
141
+ try {
142
+ fd = fs.openSync(filePath, "r")
143
+ const stats = fs.fstatSync(fd)
144
+ const limit = Math.max(0, maxBytes)
145
+ const buffer = Buffer.alloc(limit)
146
+ const bytesRead = fs.readSync(fd, buffer, 0, limit, 0)
147
+ const content = buffer.subarray(0, bytesRead).toString("utf8")
148
+ return stats.size > bytesRead ? `${content}\n...[truncated]` : content
149
+ } catch {
150
+ return null
151
+ } finally {
152
+ if (typeof fd === "number") {
153
+ try {
154
+ fs.closeSync(fd)
155
+ } catch {
156
+ // Ignore close failures; callers treat unreadable files as absent.
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ function runGit(root, command) {
163
+ try {
164
+ return execSync(command, {
165
+ cwd: root,
166
+ encoding: "utf8",
167
+ timeout: 4000,
168
+ stdio: ["ignore", "pipe", "ignore"],
169
+ }).trim()
170
+ } catch {
171
+ return null
172
+ }
173
+ }
174
+
175
+ function slugify(input) {
176
+ return (
177
+ String(input || "github-issue")
178
+ .toLowerCase()
179
+ .replace(/[^a-z0-9]+/g, "-")
180
+ .replace(/^-+|-+$/g, "")
181
+ .slice(0, 80) || "github-issue"
182
+ )
183
+ }
184
+
185
+ function ensureFallbackDir(root) {
186
+ const dir = path.join(root, FALLBACK_DIR)
187
+ fs.mkdirSync(dir, { recursive: true })
188
+ return dir
189
+ }
190
+
191
+ function getToken() {
192
+ const envTokenNames = ["GITHUB_TOKEN", "GITHUB_PAT", "CATALYST_GITHUB_TOKEN"]
193
+ for (const name of envTokenNames) {
194
+ const envToken = process.env[name]
195
+ if (envToken && envToken.trim()) {
196
+ return { ok: true, token: envToken.trim(), source: `env:${name}` }
197
+ }
198
+ }
199
+
200
+ try {
201
+ const ghToken = execSync("gh auth token", {
202
+ encoding: "utf8",
203
+ timeout: 4000,
204
+ stdio: ["ignore", "pipe", "ignore"],
205
+ }).trim()
206
+ if (ghToken) return { ok: true, token: ghToken, source: "gh-cli" }
207
+ } catch {
208
+ // gh not installed or not logged in.
209
+ }
210
+
211
+ return {
212
+ ok: false,
213
+ error: "No GitHub credentials found. Set GITHUB_TOKEN, GITHUB_PAT, or CATALYST_GITHUB_TOKEN in the MCP server environment. As an optional fallback, you can also run `gh auth login` with repo scope.",
214
+ }
215
+ }
216
+
217
+ function githubRequest(method, urlPath, token, body) {
218
+ return new Promise((resolve, reject) => {
219
+ const payload = body ? JSON.stringify(body) : null
220
+ const options = {
221
+ hostname: "api.github.com",
222
+ path: urlPath,
223
+ method,
224
+ headers: {
225
+ Authorization: `Bearer ${token}`,
226
+ Accept: "application/vnd.github+json",
227
+ "User-Agent": "catalyst-mcp/2.0",
228
+ "X-GitHub-Api-Version": "2022-11-28",
229
+ ...(payload
230
+ ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) }
231
+ : {}),
232
+ },
233
+ }
234
+
235
+ const req = https.request(options, (res) => {
236
+ let data = ""
237
+ res.on("data", (chunk) => (data += chunk))
238
+ res.on("end", () => {
239
+ try {
240
+ resolve({ status: res.statusCode, body: JSON.parse(data) })
241
+ } catch {
242
+ resolve({ status: res.statusCode, body: data })
243
+ }
244
+ })
245
+ })
246
+
247
+ req.on("error", reject)
248
+ req.setTimeout(30000, () => {
249
+ req.destroy(new Error("GitHub API request timed out after 30 seconds"))
250
+ })
251
+ if (payload) req.write(payload)
252
+ req.end()
253
+ })
254
+ }
255
+
256
+ function normalizeLabels(labels) {
257
+ if (!Array.isArray(labels)) return []
258
+ const seen = new Set()
259
+ return labels
260
+ .filter((label) => typeof label === "string")
261
+ .map((label) => label.trim().toLowerCase())
262
+ .filter((label) => {
263
+ if (!label || !DEFAULT_LABELS.includes(label) || seen.has(label)) return false
264
+ seen.add(label)
265
+ return true
266
+ })
267
+ }
268
+
269
+ function inferPrimaryLabel(title, body) {
270
+ const text = `${title || ""}\n${body || ""}`.toLowerCase()
271
+
272
+ if (/\b(dependenc|package-lock|package\.json|npm audit|vulnerab|upgrade package)\b/.test(text)) {
273
+ return "dependencies"
274
+ }
275
+ if (/\b(doc|docs|documentation|readme|guide|tutorial|typo)\b/.test(text)) {
276
+ return "documentation"
277
+ }
278
+ if (/\b(bug|broken|fail|error|exception|crash|incorrect|ignored|not working|regression)\b/.test(text)) {
279
+ return "bug"
280
+ }
281
+ if (/\b(question|how do|how to|clarify|doubt)\b/.test(text)) {
282
+ return "question"
283
+ }
284
+
285
+ return "enhancement"
286
+ }
287
+
288
+ function resolveIssueTemplate(args, body) {
289
+ const requested = typeof args.issue_template === "string" ? args.issue_template.trim().toLowerCase() : ""
290
+ if (ISSUE_TEMPLATES[requested]) return requested
291
+
292
+ const labels = normalizeLabels(args.labels)
293
+ const templateLabel = labels.find((label) => ISSUE_TEMPLATES[label])
294
+ if (templateLabel) return templateLabel
295
+
296
+ const inferred = inferPrimaryLabel(args.title, body)
297
+ return ISSUE_TEMPLATES[inferred] ? inferred : "enhancement"
298
+ }
299
+
300
+ function resolveIssueLabels(args, body) {
301
+ const labels = normalizeLabels(args.labels)
302
+ if (labels.length > 0) return labels
303
+
304
+ return [resolveIssueTemplate(args, body)]
305
+ }
306
+
307
+ function hasLabelsInput(args) {
308
+ return Object.prototype.hasOwnProperty.call(args, "labels")
309
+ }
310
+
311
+ function buildLabelSelectionResponse(args = {}) {
312
+ const textForSuggestion = [
313
+ args.title,
314
+ args.body,
315
+ args.summary,
316
+ args.current_behavior,
317
+ args.actual_behavior,
318
+ args.expected_behavior,
319
+ args.error_logs,
320
+ ]
321
+ .filter((value) => typeof value === "string" && value.trim())
322
+ .join("\n")
323
+ const suggestedLabel = textForSuggestion ? inferPrimaryLabel(args.title, textForSuggestion) : null
324
+
325
+ return {
326
+ ok: true,
327
+ dry_run: true,
328
+ label_selection_required: true,
329
+ repo: `${GITHUB_OWNER}/${GITHUB_REPO}`,
330
+ possible_labels: DEFAULT_LABELS,
331
+ label_guidance: LABEL_GUIDANCE,
332
+ suggested_label: suggestedLabel,
333
+ instructions:
334
+ "Ask the developer to select one or more labels before gathering context, rendering the issue preview, checking duplicates, or publishing. Call create_github_issue again with labels set to the selected labels.",
335
+ next_action: "Wait for developer label selection, then call create_github_issue with labels.",
336
+ }
337
+ }
338
+
339
+ function stripExistingFooter(body) {
340
+ return String(body || "")
341
+ .replace(/\n---\n(?:\*\*(?:Project|catalyst-core version|Project path|Reported via):\*\*.*\n?)+$/, "")
342
+ .trim()
343
+ }
344
+
345
+ function hasMarkdownHeadings(body) {
346
+ return /^#{2,3}\s+\S/m.test(body || "")
347
+ }
348
+
349
+ function toMarkdownList(value) {
350
+ if (Array.isArray(value)) {
351
+ return value
352
+ .map((item) => (typeof item === "string" ? item.trim() : ""))
353
+ .filter(Boolean)
354
+ .map((item, index) => {
355
+ if (/^\d+\.\s/.test(item) || /^[-*]\s/.test(item)) return item
356
+ return `${index + 1}. ${item}`
357
+ })
358
+ .join("\n")
359
+ }
360
+ return typeof value === "string" ? value.trim() : ""
361
+ }
362
+
363
+ function firstNonEmpty(...values) {
364
+ for (const value of values) {
365
+ if (typeof value === "string" && value.trim()) return value.trim()
366
+ }
367
+ return ""
368
+ }
369
+
370
+ function hasContentValue(value) {
371
+ if (typeof value === "string") return Boolean(value.trim())
372
+ if (Array.isArray(value)) return value.some(hasContentValue)
373
+ if (value && typeof value === "object") return Object.keys(value).length > 0
374
+ return false
375
+ }
376
+
377
+ function hasUserIssueContent(args = {}) {
378
+ return [
379
+ "body",
380
+ "summary",
381
+ "current_behavior",
382
+ "actual_behavior",
383
+ "expected_behavior",
384
+ "error_logs",
385
+ "environment",
386
+ "what_i_tried",
387
+ "additional_information",
388
+ "related_issues",
389
+ "root_cause",
390
+ "proposed_fix",
391
+ "optional_followups",
392
+ "steps_to_reproduce",
393
+ "repro",
394
+ "notes",
395
+ "context",
396
+ ].some((key) => hasContentValue(args[key]))
397
+ }
398
+
399
+ function appendSection(sections, heading, content) {
400
+ const text = Array.isArray(content)
401
+ ? toMarkdownList(content)
402
+ : content && typeof content === "object"
403
+ ? JSON.stringify(content, null, 2)
404
+ : firstNonEmpty(content)
405
+ if (text) sections.push(`## ${heading}\n\n${text}`)
406
+ }
407
+
408
+ function isPathInside(root, candidate) {
409
+ const resolvedRoot = path.resolve(root)
410
+ const resolvedCandidate = path.resolve(candidate)
411
+ const relative = path.relative(resolvedRoot, resolvedCandidate)
412
+ return relative === "" || (relative && !relative.startsWith("..") && !path.isAbsolute(relative))
413
+ }
414
+
415
+ function buildPreflightChecklist(args) {
416
+ const checklist = Array.isArray(args.preflight_checklist)
417
+ ? args.preflight_checklist
418
+ : [
419
+ "I have searched existing issues and this has not already been reported.",
420
+ "This is a single issue, not a bundle of unrelated issues.",
421
+ "I have included reproduction steps, environment details, and logs where available.",
422
+ ]
423
+
424
+ return checklist
425
+ .map((item) => (typeof item === "string" ? item.trim() : ""))
426
+ .filter(Boolean)
427
+ .map((item) => (item.startsWith("[") ? `- ${item}` : `- [x] ${item}`))
428
+ .join("\n")
429
+ }
430
+
431
+ function buildStyledIssueSections(args) {
432
+ const rawBody = stripExistingFooter(args.body)
433
+
434
+ if (rawBody && hasMarkdownHeadings(rawBody)) {
435
+ return rawBody
436
+ }
437
+
438
+ const sections = []
439
+ const template = resolveIssueTemplate(args, rawBody)
440
+
441
+ if (template === "documentation") {
442
+ appendSection(sections, "Summary", firstNonEmpty(args.summary, rawBody))
443
+ appendSection(
444
+ sections,
445
+ "Current Documentation",
446
+ firstNonEmpty(args.current_behavior, args.actual_behavior)
447
+ )
448
+ appendSection(
449
+ sections,
450
+ "Suggested Documentation",
451
+ firstNonEmpty(args.expected_behavior, args.proposed_fix, args.suggested_fix)
452
+ )
453
+ appendSection(sections, "Affected Pages / Examples", args.steps_to_reproduce || args.repro)
454
+ appendSection(sections, "Optional Follow-ups", args.optional_followups || args.follow_ups)
455
+ return sections.join("\n\n").trim()
456
+ }
457
+
458
+ if (template === "dependencies") {
459
+ appendSection(sections, "Summary", firstNonEmpty(args.summary, rawBody))
460
+ appendSection(
461
+ sections,
462
+ "Current Dependency",
463
+ firstNonEmpty(args.current_behavior, args.actual_behavior)
464
+ )
465
+ appendSection(sections, "Target Dependency", args.expected_behavior)
466
+ appendSection(sections, "Reason / Impact", firstNonEmpty(args.root_cause, args.notes))
467
+ appendSection(sections, "Validation Plan", args.steps_to_reproduce || args.repro)
468
+ return sections.join("\n\n").trim()
469
+ }
470
+
471
+ if (template === "question") {
472
+ appendSection(sections, "Question", firstNonEmpty(args.summary, rawBody))
473
+ appendSection(
474
+ sections,
475
+ "Context",
476
+ firstNonEmpty(args.current_behavior, args.actual_behavior, args.root_cause, args.notes)
477
+ )
478
+ appendSection(sections, "What I Tried", args.steps_to_reproduce || args.repro)
479
+ appendSection(sections, "Expected Guidance", args.expected_behavior)
480
+ return sections.join("\n\n").trim()
481
+ }
482
+
483
+ if (template === "enhancement") {
484
+ appendSection(sections, "Summary", firstNonEmpty(args.summary, rawBody))
485
+ appendSection(
486
+ sections,
487
+ "Motivation",
488
+ firstNonEmpty(args.current_behavior, args.actual_behavior, args.root_cause, args.notes)
489
+ )
490
+ appendSection(sections, "Proposed Behaviour", args.expected_behavior)
491
+ appendSection(sections, "Example Usage / Output", args.steps_to_reproduce || args.repro)
492
+ appendSection(sections, "Proposed Fix", firstNonEmpty(args.proposed_fix, args.suggested_fix))
493
+ appendSection(sections, "Optional Follow-ups", args.optional_followups || args.follow_ups)
494
+ return sections.join("\n\n").trim()
495
+ }
496
+
497
+ appendSection(sections, "Preflight Checklist", buildPreflightChecklist(args))
498
+ appendSection(sections, "What's Wrong?", firstNonEmpty(args.summary, rawBody))
499
+ appendSection(sections, "Steps to Reproduce", args.steps_to_reproduce || args.repro)
500
+ appendSection(sections, "Expected Behavior", args.expected_behavior)
501
+ appendSection(sections, "Actual Behavior", firstNonEmpty(args.actual_behavior, args.current_behavior))
502
+ appendSection(
503
+ sections,
504
+ "Error Messages/Logs",
505
+ args.error_logs ? `\`\`\`\n${args.error_logs}\n\`\`\`` : ""
506
+ )
507
+ appendSection(sections, "Environment", args.environment)
508
+ appendSection(sections, "What I Tried", args.what_i_tried || args.what_tried)
509
+ appendSection(
510
+ sections,
511
+ "Additional Information",
512
+ firstNonEmpty(
513
+ args.additional_information,
514
+ args.root_cause,
515
+ args.notes,
516
+ args.proposed_fix,
517
+ args.suggested_fix
518
+ )
519
+ )
520
+ appendSection(sections, "Related Issues", args.related_issues)
521
+
522
+ return sections.join("\n\n").trim()
523
+ }
524
+
525
+ function normalizeImages(images, root) {
526
+ if (!Array.isArray(images)) return { markdown: "", local_files: [], remote_urls: [] }
527
+
528
+ const localFiles = []
529
+ const remoteUrls = []
530
+ const lines = []
531
+
532
+ for (const image of images) {
533
+ const value = typeof image === "string" ? { path: image } : image
534
+ if (!value || typeof value !== "object") continue
535
+
536
+ const raw = value.url || value.path || value.file
537
+ if (!raw || typeof raw !== "string") continue
538
+
539
+ const alt = typeof value.alt === "string" && value.alt.trim() ? value.alt.trim() : "attachment"
540
+ if (/^https?:\/\//i.test(raw)) {
541
+ remoteUrls.push(raw)
542
+ lines.push(`![${alt}](${raw})`)
543
+ continue
544
+ }
545
+
546
+ const absolutePath = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(root, raw)
547
+ if (!isPathInside(root, absolutePath)) {
548
+ localFiles.push({ path: raw, exists: false, rejected: true, reason: "outside_project_root" })
549
+ lines.push(`- ${raw} (outside project root; not included)`)
550
+ continue
551
+ }
552
+ const exists = fs.existsSync(absolutePath)
553
+ localFiles.push({ path: absolutePath, exists })
554
+ lines.push(`- ${absolutePath}${exists ? "" : " (not found)"}`)
555
+ }
556
+
557
+ if (lines.length === 0) return { markdown: "", local_files: localFiles, remote_urls: remoteUrls }
558
+
559
+ return {
560
+ markdown: ["", "## Attachments", "", ...lines].join("\n"),
561
+ local_files: localFiles,
562
+ remote_urls: remoteUrls,
563
+ }
564
+ }
565
+
566
+ function buildIssueBody(args, options = {}) {
567
+ const root = getProjectRoot(args)
568
+ const images = normalizeImages(args.images, root)
569
+ const sections = []
570
+ const styledBody = buildStyledIssueSections(args)
571
+
572
+ if (styledBody) sections.push(styledBody)
573
+
574
+ const labels = resolveIssueLabels(args, styledBody)
575
+ if (labels.length > 0) {
576
+ sections.push(
577
+ [
578
+ "## Suggested Labels",
579
+ "",
580
+ labels.map((label) => `\`${label}\``).join(", "),
581
+ "",
582
+ "> These labels are included in the GitHub API request. If they are not visible on the issue, the token/user likely does not have permission to apply labels.",
583
+ ].join("\n")
584
+ )
585
+ }
586
+
587
+ if (args.context) {
588
+ const context =
589
+ typeof args.context === "string" ? args.context : JSON.stringify(args.context, null, 2)
590
+ sections.push(`## Catalyst/Project Context\n\n\`\`\`json\n${context}\n\`\`\``)
591
+ }
592
+
593
+ if (args.environment && resolveIssueTemplate(args, styledBody) !== "bug") {
594
+ const environment =
595
+ typeof args.environment === "string"
596
+ ? args.environment
597
+ : JSON.stringify(args.environment, null, 2)
598
+ sections.push(`## Environment\n\n${environment}`)
599
+ }
600
+
601
+ if (args.duplicate_review_note && typeof args.duplicate_review_note === "string") {
602
+ sections.push(`## Duplicate Review\n\n${args.duplicate_review_note.trim()}`)
603
+ }
604
+
605
+ if (images.markdown) sections.push(images.markdown.trim())
606
+
607
+ const body = sections.filter(Boolean).join("\n\n").trim()
608
+ return {
609
+ body: options.enrich === false ? body : enrichBody(body),
610
+ images,
611
+ }
612
+ }
613
+
614
+ function enrichBody(body) {
615
+ const lines = [stripExistingFooter(body), "", "---"]
616
+ if (_projectInfo) {
617
+ if (_projectInfo.pkg && _projectInfo.pkg.name) {
618
+ lines.push(`**Project:** \`${_projectInfo.pkg.name}\``)
619
+ }
620
+ const version = _projectInfo.installedVersion || _projectInfo.catalystVersion || "unknown"
621
+ lines.push(`**catalyst-core version:** \`${version}\``)
622
+ }
623
+ lines.push("**Reported via:** Catalyst MCP v2")
624
+ return lines.join("\n")
625
+ }
626
+
627
+ function normalizeSearchText(value) {
628
+ return String(value || "")
629
+ .toLowerCase()
630
+ .replace(/[^a-z0-9\s/_:-]/g, " ")
631
+ .replace(/\s+/g, " ")
632
+ .trim()
633
+ }
634
+
635
+ function buildDuplicateSearchText(title, body, duplicateSearchQuery) {
636
+ const explicitQuery = normalizeSearchText(duplicateSearchQuery)
637
+ if (explicitQuery) return explicitQuery
638
+
639
+ const titleQuery = normalizeSearchText(title)
640
+ if (titleQuery) return titleQuery
641
+
642
+ return normalizeSearchText(body).split(/\s+/).slice(0, 8).join(" ")
643
+ }
644
+
645
+ const SENSITIVE_FILE_PATTERN =
646
+ /(^|\/)(\.env|\.npmrc|\.pypirc|config\/config\.json|google-services\.json|googleservice-info\.plist|.*keystore.*|.*\.jks|.*\.p12|.*\.pem|.*\.key)$/i
647
+
648
+ const SENSITIVE_TEXT_PATTERN =
649
+ /\b(api[_-]?key|auth[_-]?token|authorization|client[_-]?secret|cookie|firebase|google[_-]?services|password|private[_-]?key|secret|session[_-]?id|sentry[_-]?dsn|token)\b/i
650
+
651
+ const SENSITIVE_BODY_PATTERN =
652
+ /\b(api[_-]?key|auth[_-]?token|authorization|client[_-]?secret|cookie|password|private[_-]?key|secret|session[_-]?id|sentry[_-]?dsn|token)\b\s*[:=]\s*\S+|\bbearer\s+[a-z0-9._-]+/i
653
+
654
+ function addSensitiveFinding(findings, seen, finding) {
655
+ const key = `${finding.type}:${finding.path || ""}:${finding.reason}`
656
+ if (seen.has(key)) return
657
+ seen.add(key)
658
+ findings.push(finding)
659
+ }
660
+
661
+ function detectSensitiveIssueData({ body, context }) {
662
+ const findings = []
663
+ const seen = new Set()
664
+
665
+ if (context && context.config) {
666
+ addSensitiveFinding(findings, seen, {
667
+ type: "config",
668
+ path: "config/config.json",
669
+ reason: "Issue context includes Catalyst config fields.",
670
+ })
671
+ }
672
+
673
+ const relatedFiles = context && Array.isArray(context.related_files) ? context.related_files : []
674
+ for (const file of relatedFiles) {
675
+ if (!file || typeof file.path !== "string") continue
676
+
677
+ if (SENSITIVE_FILE_PATTERN.test(file.path)) {
678
+ addSensitiveFinding(findings, seen, {
679
+ type: "file",
680
+ path: file.path,
681
+ reason: "Related file path is a config or credential-looking file.",
682
+ })
683
+ }
684
+
685
+ if (typeof file.content === "string" && SENSITIVE_TEXT_PATTERN.test(file.content)) {
686
+ addSensitiveFinding(findings, seen, {
687
+ type: "content",
688
+ path: file.path,
689
+ reason: "Related file content contains secret-looking keys or values.",
690
+ })
691
+ }
692
+ }
693
+
694
+ if (typeof body === "string" && SENSITIVE_BODY_PATTERN.test(body)) {
695
+ addSensitiveFinding(findings, seen, {
696
+ type: "body",
697
+ path: null,
698
+ reason: "Rendered issue body contains sensitive-looking words or values.",
699
+ })
700
+ }
701
+
702
+ return {
703
+ required_before_publish: findings.length > 0,
704
+ status: findings.length > 0 ? "needs_user_review" : "no_sensitive_or_config_data_detected",
705
+ findings,
706
+ publish_override_field:
707
+ findings.length > 0
708
+ ? "Pass sensitive_data_confirmed:true with dry_run:false only after the developer reviews the preview and confirms the config/sensitive-looking data may be posted."
709
+ : null,
710
+ }
711
+ }
712
+
713
+ async function searchDuplicates(token, title, body, duplicateSearchQuery) {
714
+ const searchText = buildDuplicateSearchText(title, body, duplicateSearchQuery)
715
+ if (!searchText) return []
716
+
717
+ try {
718
+ const q = encodeURIComponent(
719
+ `repo:${GITHUB_OWNER}/${GITHUB_REPO} ${searchText} in:title,body is:issue`
720
+ )
721
+ const res = await githubRequest("GET", `/search/issues?q=${q}&per_page=5`, token)
722
+ if (res.status === 200 && Array.isArray(res.body.items)) {
723
+ return res.body.items.slice(0, 5).map((issue) => ({
724
+ number: issue.number,
725
+ title: issue.title,
726
+ url: issue.html_url,
727
+ state: issue.state,
728
+ }))
729
+ }
730
+ } catch {
731
+ // Duplicate detection is best-effort.
732
+ }
733
+ return []
734
+ }
735
+
736
+ async function addLabelsToIssue(token, issueNumber, labels) {
737
+ if (!Array.isArray(labels) || labels.length === 0) {
738
+ return { ok: true, labels: [] }
739
+ }
740
+
741
+ const res = await githubRequest(
742
+ "POST",
743
+ `/repos/${GITHUB_OWNER}/${GITHUB_REPO}/issues/${issueNumber}/labels`,
744
+ token,
745
+ { labels }
746
+ )
747
+
748
+ if (res.status === 200 && Array.isArray(res.body)) {
749
+ return {
750
+ ok: true,
751
+ labels: res.body.map((label) => label.name),
752
+ }
753
+ }
754
+
755
+ const apiMessage = res.body && res.body.message ? res.body.message : JSON.stringify(res.body)
756
+ return {
757
+ ok: false,
758
+ status: res.status,
759
+ error: `GitHub API returned HTTP ${res.status}: ${apiMessage}`,
760
+ }
761
+ }
762
+
763
+ async function commentOnIssue(token, issueNumber, body) {
764
+ if (!body || typeof body !== "string" || !body.trim()) {
765
+ return { ok: true }
766
+ }
767
+
768
+ const res = await githubRequest(
769
+ "POST",
770
+ `/repos/${GITHUB_OWNER}/${GITHUB_REPO}/issues/${issueNumber}/comments`,
771
+ token,
772
+ { body: body.trim() }
773
+ )
774
+
775
+ if (res.status === 201) {
776
+ return { ok: true, url: res.body.html_url }
777
+ }
778
+
779
+ const apiMessage = res.body && res.body.message ? res.body.message : JSON.stringify(res.body)
780
+ return {
781
+ ok: false,
782
+ status: res.status,
783
+ error: `GitHub API returned HTTP ${res.status}: ${apiMessage}`,
784
+ }
785
+ }
786
+
787
+ function collectRelatedFiles(root, query, explicitFiles) {
788
+ const files = []
789
+ const candidates = Array.isArray(explicitFiles) ? explicitFiles : []
790
+ for (const file of candidates) {
791
+ if (typeof file !== "string") continue
792
+ const absolute = path.isAbsolute(file) ? path.resolve(file) : path.resolve(root, file)
793
+ if (!isPathInside(root, absolute)) continue
794
+ const content = safeReadText(absolute, 12000)
795
+ if (content) {
796
+ files.push({ path: path.relative(root, absolute), content })
797
+ }
798
+ }
799
+
800
+ if (files.length > 0 || !query) return files
801
+
802
+ const words = normalizeSearchText(query).split(/\s+/).filter(Boolean).slice(0, 5)
803
+ if (words.length === 0) return files
804
+
805
+ const srcDir = path.join(root, "src")
806
+ const matches = []
807
+ function walk(dir) {
808
+ let entries
809
+ try {
810
+ entries = fs.readdirSync(dir, { withFileTypes: true })
811
+ } catch {
812
+ return
813
+ }
814
+ for (const entry of entries) {
815
+ if (entry.name === "node_modules" || entry.name === ".git") continue
816
+ const full = path.join(dir, entry.name)
817
+ if (entry.isDirectory()) {
818
+ walk(full)
819
+ } else if (/\.(js|jsx|ts|tsx|json|scss|css|md|mdx)$/.test(entry.name)) {
820
+ const content = safeReadText(full, 4000)
821
+ if (content && words.some((word) => content.toLowerCase().includes(word))) {
822
+ matches.push({ path: path.relative(root, full), content })
823
+ }
824
+ }
825
+ if (matches.length >= 5) return
826
+ }
827
+ }
828
+ walk(srcDir)
829
+ return matches.slice(0, 5)
830
+ }
831
+
832
+ function gatherGithubIssueContext(args = {}) {
833
+ const root = getProjectRoot(args)
834
+ const pkg = safeReadJson(path.join(root, "package.json"))
835
+ const configJson = safeReadJson(path.join(root, "config", "config.json"))
836
+ const branch = runGit(root, "git rev-parse --abbrev-ref HEAD")
837
+ const commit = runGit(root, "git rev-parse --short HEAD")
838
+ const status = runGit(root, "git status --short")
839
+ const nodeVersion = runGit(root, "node --version")
840
+ const npmVersion = runGit(root, "npm --version")
841
+ const relatedFiles = collectRelatedFiles(root, args.query || args.problem || args.title || "", args.files)
842
+
843
+ const dependencies = pkg ? { ...pkg.dependencies, ...pkg.devDependencies } : {}
844
+
845
+ return {
846
+ project_path: root,
847
+ package_name: pkg && pkg.name,
848
+ catalyst_core_declared_version: dependencies["catalyst-core"] || null,
849
+ catalyst_core_installed_version: _projectInfo && _projectInfo.installedVersion,
850
+ scripts: pkg && pkg.scripts ? pkg.scripts : {},
851
+ config: configJson
852
+ ? {
853
+ NODE_SERVER_PORT: configJson.NODE_SERVER_PORT,
854
+ WEBVIEW_CONFIG: configJson.WEBVIEW_CONFIG,
855
+ }
856
+ : null,
857
+ git: { branch, commit, dirty: Boolean(status), status },
858
+ runtime: { node: nodeVersion, npm: npmVersion },
859
+ related_files: relatedFiles,
860
+ }
861
+ }
862
+
863
+ function generateIssueMarkdown(args = {}) {
864
+ const { title } = args
865
+ if (!title || typeof title !== "string" || title.trim() === "") {
866
+ return { ok: false, error: "title is required." }
867
+ }
868
+
869
+ const root = getProjectRoot(args)
870
+ if (!hasUserIssueContent(args)) {
871
+ return { ok: false, error: "body or structured issue fields are required." }
872
+ }
873
+
874
+ const built = buildIssueBody(args, { enrich: true })
875
+ const labels = resolveIssueLabels(args, built.body)
876
+ const dir = ensureFallbackDir(root)
877
+ const filePath = path.join(dir, `${new Date().toISOString().replace(/[:.]/g, "-")}-${slugify(title)}.md`)
878
+
879
+ const markdown = [
880
+ `# ${title.trim()}`,
881
+ "",
882
+ labels.length ? `Labels: ${labels.join(", ")}` : "Labels: none",
883
+ "",
884
+ `Repository: https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/issues/new`,
885
+ "",
886
+ "Copy the title, labels, and body below into GitHub. Upload any local image files listed under Attachments manually.",
887
+ "",
888
+ "## Body",
889
+ "",
890
+ built.body,
891
+ "",
892
+ ].join("\n")
893
+
894
+ fs.writeFileSync(filePath, markdown, "utf8")
895
+
896
+ return {
897
+ ok: true,
898
+ markdown_path: filePath,
899
+ title: title.trim(),
900
+ labels,
901
+ local_images: built.images.local_files,
902
+ github_new_issue_url: `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/issues/new`,
903
+ }
904
+ }
905
+
906
+ async function handle_create_github_issue(args = {}) {
907
+ const labels = normalizeLabels(args.labels)
908
+ if (!hasLabelsInput(args) || labels.length === 0) {
909
+ if (hasLabelsInput(args)) {
910
+ return {
911
+ ok: false,
912
+ error: "labels must include at least one valid Catalyst GitHub label.",
913
+ possible_labels: DEFAULT_LABELS,
914
+ label_guidance: LABEL_GUIDANCE,
915
+ }
916
+ }
917
+ return buildLabelSelectionResponse(args)
918
+ }
919
+
920
+ const { title } = args
921
+ if (!title || typeof title !== "string" || title.trim() === "") {
922
+ return { ok: false, error: "title is required and must be a non-empty string." }
923
+ }
924
+
925
+ if (!hasUserIssueContent(args)) {
926
+ return { ok: false, error: "body or structured issue fields are required." }
927
+ }
928
+
929
+ const dryRun = args.dry_run !== false
930
+ const gatheredContext = gatherGithubIssueContext({
931
+ ...args,
932
+ query: args.query || args.duplicate_search_query || args.summary || args.body || args.title,
933
+ })
934
+ const issueArgs = {
935
+ ...args,
936
+ context: args.context,
937
+ }
938
+ const built = buildIssueBody(issueArgs)
939
+ if (!built.body || built.body.trim() === "") {
940
+ return { ok: false, error: "body or structured issue fields are required." }
941
+ }
942
+
943
+ let duplicates = []
944
+ let tokenSource = "none"
945
+ const tokenResult = getToken()
946
+ if (tokenResult.ok) {
947
+ tokenSource = tokenResult.source
948
+ duplicates = await searchDuplicates(
949
+ tokenResult.token,
950
+ title,
951
+ built.body,
952
+ issueArgs.duplicate_search_query
953
+ )
954
+ }
955
+
956
+ const preview = {
957
+ type: "issue",
958
+ repo: `${GITHUB_OWNER}/${GITHUB_REPO}`,
959
+ title: title.trim(),
960
+ body: built.body,
961
+ labels,
962
+ suggested_template: ISSUE_TEMPLATES[resolveIssueTemplate(issueArgs, built.body)],
963
+ possible_labels: DEFAULT_LABELS,
964
+ label_guidance: LABEL_GUIDANCE,
965
+ images: built.images,
966
+ context: gatheredContext,
967
+ }
968
+
969
+ const duplicateReview = {
970
+ required_before_publish: duplicates.length > 0,
971
+ status: duplicates.length > 0 ? "needs_user_review" : "no_duplicate_candidates_found",
972
+ candidates: duplicates,
973
+ publish_override_field:
974
+ duplicates.length > 0
975
+ ? "Pass duplicate_review_confirmed:true with dry_run:false only after the developer confirms this is not a duplicate."
976
+ : null,
977
+ }
978
+ const sensitiveDataReview = detectSensitiveIssueData({
979
+ body: built.body,
980
+ context: issueArgs.context,
981
+ })
982
+
983
+ if (dryRun) {
984
+ return {
985
+ ok: true,
986
+ dry_run: true,
987
+ preview,
988
+ duplicates,
989
+ duplicate_review: duplicateReview,
990
+ sensitive_data_review: sensitiveDataReview,
991
+ token_source: tokenSource,
992
+ auth_warning: tokenResult.ok ? null : tokenResult.error,
993
+ instructions:
994
+ duplicates.length > 0 || sensitiveDataReview.required_before_publish
995
+ ? "Show the rendered issue preview, suggested template, labels, attachments, context, duplicate candidates if any, and sensitive/config data warning if present. Ask whether to edit, cancel, use an existing issue, or publish. Publish only after explicit approval with dry_run:false. Include duplicate_review_confirmed:true if duplicates were reviewed, and sensitive_data_confirmed:true if config or sensitive-looking data was detected and the developer confirms it may be posted."
996
+ : "Show the rendered issue preview, suggested template, labels, attachments, and context to the developer. Ask whether to edit, cancel, or publish. Publish only after explicit approval with dry_run:false.",
997
+ next_action:
998
+ "Wait for explicit developer approval before calling create_github_issue with dry_run:false.",
999
+ }
1000
+ }
1001
+
1002
+ const payload = {
1003
+ title: title.trim(),
1004
+ body: built.body,
1005
+ labels,
1006
+ }
1007
+
1008
+ if (sensitiveDataReview.required_before_publish && args.sensitive_data_confirmed !== true) {
1009
+ return {
1010
+ ok: false,
1011
+ blocked: true,
1012
+ error: "The rendered GitHub issue includes config or sensitive-looking data. Show the preview and warning to the developer, and publish only if they explicitly confirm this data may be posted.",
1013
+ sensitive_data_review: sensitiveDataReview,
1014
+ next_action:
1015
+ "If the developer confirms the config/sensitive-looking data may be posted, call create_github_issue again with sensitive_data_confirmed:true and dry_run:false.",
1016
+ }
1017
+ }
1018
+
1019
+ if (!tokenResult.ok) {
1020
+ const fallback = generateIssueMarkdown(issueArgs)
1021
+ return {
1022
+ ok: false,
1023
+ error: tokenResult.error,
1024
+ fallback: fallback.ok ? fallback : null,
1025
+ }
1026
+ }
1027
+
1028
+ try {
1029
+ if (duplicates.length > 0 && args.duplicate_review_confirmed !== true) {
1030
+ return {
1031
+ ok: false,
1032
+ blocked: true,
1033
+ error: "Possible duplicate GitHub issues were found. Show these candidates to the developer and publish only if they confirm this report is distinct.",
1034
+ duplicate_candidates: duplicates,
1035
+ next_action:
1036
+ "If the developer confirms this is not a duplicate, call create_github_issue again with duplicate_review_confirmed:true and optionally duplicate_review_note.",
1037
+ }
1038
+ }
1039
+
1040
+ const res = await githubRequest(
1041
+ "POST",
1042
+ `/repos/${GITHUB_OWNER}/${GITHUB_REPO}/issues`,
1043
+ tokenResult.token,
1044
+ payload
1045
+ )
1046
+
1047
+ if (res.status === 201) {
1048
+ const returnedLabels = (res.body.labels || []).map((label) => label.name)
1049
+ const missingLabels = labels.filter((label) => !returnedLabels.includes(label))
1050
+ let finalLabels = returnedLabels
1051
+ let labelWarning = null
1052
+
1053
+ if (missingLabels.length > 0) {
1054
+ const labelResult = await addLabelsToIssue(tokenResult.token, res.body.number, missingLabels)
1055
+ if (labelResult.ok) {
1056
+ finalLabels = labelResult.labels
1057
+ } else {
1058
+ labelWarning =
1059
+ `Issue was created, but GitHub did not apply label(s): ${missingLabels.join(", ")}. ` +
1060
+ `This usually means the token/user can create issues but does not have triage/write permission to manage labels. ${labelResult.error}`
1061
+ const commentResult = await commentOnIssue(
1062
+ tokenResult.token,
1063
+ res.body.number,
1064
+ [
1065
+ "Catalyst MCP could not apply the requested GitHub label(s).",
1066
+ "",
1067
+ `Requested labels: ${missingLabels.map((label) => `\`${label}\``).join(", ")}`,
1068
+ "",
1069
+ "Please add these labels manually if appropriate. This usually means the token/user can create issues but does not have triage/write permission to manage labels.",
1070
+ ].join("\n")
1071
+ )
1072
+ if (!commentResult.ok) {
1073
+ labelWarning += ` MCP also could not add a label-warning comment. ${commentResult.error}`
1074
+ }
1075
+ }
1076
+ }
1077
+
1078
+ return {
1079
+ ok: true,
1080
+ message: labelWarning
1081
+ ? `Issue #${res.body.number} created successfully, but labels need manual review.`
1082
+ : `Issue #${res.body.number} created successfully.`,
1083
+ number: res.body.number,
1084
+ title: res.body.title,
1085
+ url: res.body.html_url,
1086
+ labels: finalLabels,
1087
+ requested_labels: labels,
1088
+ label_warning: labelWarning,
1089
+ state: res.body.state,
1090
+ token_source: tokenResult.source,
1091
+ local_images_requiring_manual_upload: built.images.local_files.filter(
1092
+ (image) => image.exists
1093
+ ),
1094
+ }
1095
+ }
1096
+
1097
+ const apiMessage = res.body && res.body.message ? res.body.message : JSON.stringify(res.body)
1098
+ const fallback = generateIssueMarkdown(issueArgs)
1099
+ return {
1100
+ ok: false,
1101
+ error: `GitHub API returned HTTP ${res.status}: ${apiMessage}`,
1102
+ fallback: fallback.ok ? fallback : null,
1103
+ }
1104
+ } catch (err) {
1105
+ const fallback = generateIssueMarkdown(issueArgs)
1106
+ return {
1107
+ ok: false,
1108
+ error: `Network error while calling GitHub API: ${err.message}`,
1109
+ fallback: fallback.ok ? fallback : null,
1110
+ }
1111
+ }
1112
+ }
1113
+
1114
+ module.exports = {
1115
+ init,
1116
+ handle_create_github_issue,
1117
+ }