@intrect/openswarm 0.15.0 → 0.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 (323) hide show
  1. package/README.md +4 -3
  2. package/dist/adapters/agenticLoop.d.ts +2 -0
  3. package/dist/adapters/agenticLoop.d.ts.map +1 -1
  4. package/dist/adapters/agenticLoop.js +8 -5
  5. package/dist/adapters/agenticLoop.js.map +1 -1
  6. package/dist/adapters/codexResponses.d.ts.map +1 -1
  7. package/dist/adapters/codexResponses.js +1 -0
  8. package/dist/adapters/codexResponses.js.map +1 -1
  9. package/dist/adapters/gpt.d.ts.map +1 -1
  10. package/dist/adapters/gpt.js +1 -0
  11. package/dist/adapters/gpt.js.map +1 -1
  12. package/dist/adapters/local.d.ts.map +1 -1
  13. package/dist/adapters/local.js +1 -0
  14. package/dist/adapters/local.js.map +1 -1
  15. package/dist/adapters/openrouter.d.ts.map +1 -1
  16. package/dist/adapters/openrouter.js +1 -0
  17. package/dist/adapters/openrouter.js.map +1 -1
  18. package/dist/adapters/tools.d.ts +2 -0
  19. package/dist/adapters/tools.d.ts.map +1 -1
  20. package/dist/adapters/tools.js +7 -0
  21. package/dist/adapters/tools.js.map +1 -1
  22. package/dist/adapters/types.d.ts +2 -0
  23. package/dist/adapters/types.d.ts.map +1 -1
  24. package/dist/automation/autonomousRunner.d.ts +5 -5
  25. package/dist/automation/autonomousRunner.d.ts.map +1 -1
  26. package/dist/automation/autonomousRunner.js +7 -9
  27. package/dist/automation/autonomousRunner.js.map +1 -1
  28. package/dist/automation/ciWorker.d.ts.map +1 -1
  29. package/dist/automation/ciWorker.js +16 -11
  30. package/dist/automation/ciWorker.js.map +1 -1
  31. package/dist/automation/dailyReporter.d.ts.map +1 -1
  32. package/dist/automation/dailyReporter.js +7 -2
  33. package/dist/automation/dailyReporter.js.map +1 -1
  34. package/dist/automation/longRunningMonitor.d.ts.map +1 -1
  35. package/dist/automation/longRunningMonitor.js +17 -27
  36. package/dist/automation/longRunningMonitor.js.map +1 -1
  37. package/dist/automation/prProcessor.d.ts.map +1 -1
  38. package/dist/automation/prProcessor.js +103 -24
  39. package/dist/automation/prProcessor.js.map +1 -1
  40. package/dist/automation/runnerState.d.ts +0 -6
  41. package/dist/automation/runnerState.d.ts.map +1 -1
  42. package/dist/automation/runnerState.js +16 -31
  43. package/dist/automation/runnerState.js.map +1 -1
  44. package/dist/automation/runnerTypes.d.ts +0 -2
  45. package/dist/automation/runnerTypes.d.ts.map +1 -1
  46. package/dist/automation/scheduler.d.ts.map +1 -1
  47. package/dist/automation/scheduler.js +76 -76
  48. package/dist/automation/scheduler.js.map +1 -1
  49. package/dist/automation/taskSource.d.ts +1 -1
  50. package/dist/automation/taskSource.d.ts.map +1 -1
  51. package/dist/automation/taskSource.js +95 -8
  52. package/dist/automation/taskSource.js.map +1 -1
  53. package/dist/automation/workerAuditLog.d.ts.map +1 -1
  54. package/dist/automation/workerAuditLog.js +4 -1
  55. package/dist/automation/workerAuditLog.js.map +1 -1
  56. package/dist/cli/authHandler.d.ts.map +1 -1
  57. package/dist/cli/authHandler.js +10 -12
  58. package/dist/cli/authHandler.js.map +1 -1
  59. package/dist/cli/checkHandler.d.ts.map +1 -1
  60. package/dist/cli/checkHandler.js +12 -10
  61. package/dist/cli/checkHandler.js.map +1 -1
  62. package/dist/cli/daemon.d.ts.map +1 -1
  63. package/dist/cli/daemon.js +24 -9
  64. package/dist/cli/daemon.js.map +1 -1
  65. package/dist/cli/fixCommand.d.ts.map +1 -1
  66. package/dist/cli/fixCommand.js +10 -0
  67. package/dist/cli/fixCommand.js.map +1 -1
  68. package/dist/cli/initWizard.d.ts.map +1 -1
  69. package/dist/cli/initWizard.js +6 -1
  70. package/dist/cli/initWizard.js.map +1 -1
  71. package/dist/cli/projectHandler.d.ts.map +1 -1
  72. package/dist/cli/projectHandler.js +3 -2
  73. package/dist/cli/projectHandler.js.map +1 -1
  74. package/dist/cli/promptHandler.d.ts.map +1 -1
  75. package/dist/cli/promptHandler.js +21 -10
  76. package/dist/cli/promptHandler.js.map +1 -1
  77. package/dist/cli/reviewAudit.d.ts +2 -2
  78. package/dist/cli/reviewAudit.js +3 -3
  79. package/dist/cli/reviewAudit.js.map +1 -1
  80. package/dist/cli/reviewMaxCommand.d.ts.map +1 -1
  81. package/dist/cli/reviewMaxCommand.js +10 -2
  82. package/dist/cli/reviewMaxCommand.js.map +1 -1
  83. package/dist/cli/scheduleCommand.d.ts.map +1 -1
  84. package/dist/cli/scheduleCommand.js +20 -5
  85. package/dist/cli/scheduleCommand.js.map +1 -1
  86. package/dist/cli.js +15 -4
  87. package/dist/cli.js.map +1 -1
  88. package/dist/core/config.d.ts +33 -14
  89. package/dist/core/config.d.ts.map +1 -1
  90. package/dist/core/config.js +18 -3
  91. package/dist/core/config.js.map +1 -1
  92. package/dist/core/service.d.ts.map +1 -1
  93. package/dist/core/service.js +0 -2
  94. package/dist/core/service.js.map +1 -1
  95. package/dist/core/types.d.ts +0 -2
  96. package/dist/core/types.d.ts.map +1 -1
  97. package/dist/discord/discordCore.d.ts.map +1 -1
  98. package/dist/discord/discordCore.js +20 -4
  99. package/dist/discord/discordCore.js.map +1 -1
  100. package/dist/discord/discordPair.js +12 -0
  101. package/dist/discord/discordPair.js.map +1 -1
  102. package/dist/github/github.d.ts +1 -1
  103. package/dist/github/github.d.ts.map +1 -1
  104. package/dist/github/github.js +41 -11
  105. package/dist/github/github.js.map +1 -1
  106. package/dist/index.js +15 -6
  107. package/dist/index.js.map +1 -1
  108. package/dist/issues/graphql/resolvers.d.ts +4 -4
  109. package/dist/issues/graphql/resolvers.d.ts.map +1 -1
  110. package/dist/issues/graphql/resolvers.js +43 -3
  111. package/dist/issues/graphql/resolvers.js.map +1 -1
  112. package/dist/issues/graphql/server.d.ts.map +1 -1
  113. package/dist/issues/graphql/server.js +76 -5
  114. package/dist/issues/graphql/server.js.map +1 -1
  115. package/dist/issues/graphql/typeDefs.d.ts +1 -1
  116. package/dist/issues/graphql/typeDefs.d.ts.map +1 -1
  117. package/dist/issues/graphql/typeDefs.js +0 -5
  118. package/dist/issues/graphql/typeDefs.js.map +1 -1
  119. package/dist/issues/issueBoardHtml.d.ts +1 -1
  120. package/dist/issues/issueBoardHtml.d.ts.map +1 -1
  121. package/dist/issues/issueBoardHtml.js +40 -20
  122. package/dist/issues/issueBoardHtml.js.map +1 -1
  123. package/dist/issues/linearBridge.d.ts +1 -1
  124. package/dist/issues/linearBridge.d.ts.map +1 -1
  125. package/dist/issues/linearBridge.js +14 -5
  126. package/dist/issues/linearBridge.js.map +1 -1
  127. package/dist/issues/memoryBridge.d.ts.map +1 -1
  128. package/dist/issues/memoryBridge.js +23 -11
  129. package/dist/issues/memoryBridge.js.map +1 -1
  130. package/dist/issues/schema.d.ts +3 -3
  131. package/dist/issues/sqliteStore.d.ts +3 -0
  132. package/dist/issues/sqliteStore.d.ts.map +1 -1
  133. package/dist/issues/sqliteStore.js +124 -19
  134. package/dist/issues/sqliteStore.js.map +1 -1
  135. package/dist/knowledge/analyzer.d.ts.map +1 -1
  136. package/dist/knowledge/analyzer.js +13 -1
  137. package/dist/knowledge/analyzer.js.map +1 -1
  138. package/dist/knowledge/graphqlExporter.d.ts.map +1 -1
  139. package/dist/knowledge/graphqlExporter.js +38 -9
  140. package/dist/knowledge/graphqlExporter.js.map +1 -1
  141. package/dist/knowledge/scanner.d.ts.map +1 -1
  142. package/dist/knowledge/scanner.js +69 -26
  143. package/dist/knowledge/scanner.js.map +1 -1
  144. package/dist/linear/linear.d.ts.map +1 -1
  145. package/dist/linear/linear.js +11 -11
  146. package/dist/linear/linear.js.map +1 -1
  147. package/dist/linear/projectUpdater.js +12 -2
  148. package/dist/linear/projectUpdater.js.map +1 -1
  149. package/dist/locale/en.d.ts.map +1 -1
  150. package/dist/locale/en.js +4 -0
  151. package/dist/locale/en.js.map +1 -1
  152. package/dist/locale/index.d.ts +7 -2
  153. package/dist/locale/index.d.ts.map +1 -1
  154. package/dist/locale/index.js.map +1 -1
  155. package/dist/locale/ko.d.ts.map +1 -1
  156. package/dist/locale/ko.js +4 -0
  157. package/dist/locale/ko.js.map +1 -1
  158. package/dist/locale/prompts/en.d.ts.map +1 -1
  159. package/dist/locale/prompts/en.js +111 -42
  160. package/dist/locale/prompts/en.js.map +1 -1
  161. package/dist/locale/prompts/ko.d.ts.map +1 -1
  162. package/dist/locale/prompts/ko.js +111 -42
  163. package/dist/locale/prompts/ko.js.map +1 -1
  164. package/dist/locale/types.d.ts +4 -0
  165. package/dist/locale/types.d.ts.map +1 -1
  166. package/dist/mcp/mcpClient.d.ts.map +1 -1
  167. package/dist/mcp/mcpClient.js +68 -6
  168. package/dist/mcp/mcpClient.js.map +1 -1
  169. package/dist/mcp/memoryServer.js +3 -0
  170. package/dist/mcp/memoryServer.js.map +1 -1
  171. package/dist/memory/codex.d.ts.map +1 -1
  172. package/dist/memory/codex.js +3 -2
  173. package/dist/memory/codex.js.map +1 -1
  174. package/dist/memory/compaction.d.ts.map +1 -1
  175. package/dist/memory/compaction.js +36 -6
  176. package/dist/memory/compaction.js.map +1 -1
  177. package/dist/memory/memoryCore.d.ts +1 -0
  178. package/dist/memory/memoryCore.d.ts.map +1 -1
  179. package/dist/memory/memoryCore.js +43 -25
  180. package/dist/memory/memoryCore.js.map +1 -1
  181. package/dist/memory/memoryOps.d.ts.map +1 -1
  182. package/dist/memory/memoryOps.js +54 -58
  183. package/dist/memory/memoryOps.js.map +1 -1
  184. package/dist/notify/notifier.d.ts.map +1 -1
  185. package/dist/notify/notifier.js +31 -7
  186. package/dist/notify/notifier.js.map +1 -1
  187. package/dist/orchestration/conflictDetector.d.ts.map +1 -1
  188. package/dist/orchestration/conflictDetector.js +15 -4
  189. package/dist/orchestration/conflictDetector.js.map +1 -1
  190. package/dist/orchestration/decisionEngine.d.ts +32 -2
  191. package/dist/orchestration/decisionEngine.d.ts.map +1 -1
  192. package/dist/orchestration/decisionEngine.js +80 -41
  193. package/dist/orchestration/decisionEngine.js.map +1 -1
  194. package/dist/orchestration/taskParser.d.ts.map +1 -1
  195. package/dist/orchestration/taskParser.js +21 -3
  196. package/dist/orchestration/taskParser.js.map +1 -1
  197. package/dist/orchestration/taskScheduler.d.ts.map +1 -1
  198. package/dist/orchestration/taskScheduler.js +47 -19
  199. package/dist/orchestration/taskScheduler.js.map +1 -1
  200. package/dist/orchestration/workflow.d.ts.map +1 -1
  201. package/dist/orchestration/workflow.js +22 -5
  202. package/dist/orchestration/workflow.js.map +1 -1
  203. package/dist/registry/bsDetector.js +2 -2
  204. package/dist/registry/bsDetector.js.map +1 -1
  205. package/dist/registry/entityScanner.d.ts.map +1 -1
  206. package/dist/registry/entityScanner.js +25 -6
  207. package/dist/registry/entityScanner.js.map +1 -1
  208. package/dist/registry/graphql/resolvers.d.ts +23 -8
  209. package/dist/registry/graphql/resolvers.d.ts.map +1 -1
  210. package/dist/registry/graphql/resolvers.js +126 -32
  211. package/dist/registry/graphql/resolvers.js.map +1 -1
  212. package/dist/registry/graphql/typeDefs.d.ts +1 -1
  213. package/dist/registry/graphql/typeDefs.d.ts.map +1 -1
  214. package/dist/registry/graphql/typeDefs.js +9 -8
  215. package/dist/registry/graphql/typeDefs.js.map +1 -1
  216. package/dist/registry/issueBridge.d.ts +1 -1
  217. package/dist/registry/issueBridge.d.ts.map +1 -1
  218. package/dist/registry/issueBridge.js +2 -2
  219. package/dist/registry/issueBridge.js.map +1 -1
  220. package/dist/registry/sqliteStore.d.ts +2 -2
  221. package/dist/registry/sqliteStore.d.ts.map +1 -1
  222. package/dist/registry/sqliteStore.js +24 -10
  223. package/dist/registry/sqliteStore.js.map +1 -1
  224. package/dist/runners/cliRunner.d.ts.map +1 -1
  225. package/dist/runners/cliRunner.js +42 -19
  226. package/dist/runners/cliRunner.js.map +1 -1
  227. package/dist/support/apiCache.d.ts +15 -1
  228. package/dist/support/apiCache.d.ts.map +1 -1
  229. package/dist/support/apiCache.js +42 -7
  230. package/dist/support/apiCache.js.map +1 -1
  231. package/dist/support/chatBackend.d.ts.map +1 -1
  232. package/dist/support/chatBackend.js +2 -3
  233. package/dist/support/chatBackend.js.map +1 -1
  234. package/dist/support/chatSession.d.ts.map +1 -1
  235. package/dist/support/chatSession.js +21 -3
  236. package/dist/support/chatSession.js.map +1 -1
  237. package/dist/support/dashboardHtml.d.ts +1 -1
  238. package/dist/support/dashboardHtml.d.ts.map +1 -1
  239. package/dist/support/dashboardHtml.js +19 -14
  240. package/dist/support/dashboardHtml.js.map +1 -1
  241. package/dist/support/delete-beliefs.d.ts +3 -1
  242. package/dist/support/delete-beliefs.d.ts.map +1 -1
  243. package/dist/support/delete-beliefs.js +18 -7
  244. package/dist/support/delete-beliefs.js.map +1 -1
  245. package/dist/support/editParser.d.ts.map +1 -1
  246. package/dist/support/editParser.js +46 -12
  247. package/dist/support/editParser.js.map +1 -1
  248. package/dist/support/gitTracker.d.ts +2 -2
  249. package/dist/support/gitTracker.d.ts.map +1 -1
  250. package/dist/support/gitTracker.js +10 -5
  251. package/dist/support/gitTracker.js.map +1 -1
  252. package/dist/support/planner.d.ts.map +1 -1
  253. package/dist/support/planner.js +1 -0
  254. package/dist/support/planner.js.map +1 -1
  255. package/dist/support/projectMapper.d.ts.map +1 -1
  256. package/dist/support/projectMapper.js +21 -16
  257. package/dist/support/projectMapper.js.map +1 -1
  258. package/dist/support/rateLimiter.d.ts.map +1 -1
  259. package/dist/support/rateLimiter.js +3 -0
  260. package/dist/support/rateLimiter.js.map +1 -1
  261. package/dist/support/rollback.d.ts.map +1 -1
  262. package/dist/support/rollback.js +57 -8
  263. package/dist/support/rollback.js.map +1 -1
  264. package/dist/support/stuckDetector.d.ts.map +1 -1
  265. package/dist/support/stuckDetector.js +8 -6
  266. package/dist/support/stuckDetector.js.map +1 -1
  267. package/dist/support/timeWindow.d.ts +1 -1
  268. package/dist/support/timeWindow.d.ts.map +1 -1
  269. package/dist/support/timeWindow.js +18 -7
  270. package/dist/support/timeWindow.js.map +1 -1
  271. package/dist/support/web.d.ts.map +1 -1
  272. package/dist/support/web.js +872 -791
  273. package/dist/support/web.js.map +1 -1
  274. package/dist/support/workflowLinear.d.ts.map +1 -1
  275. package/dist/support/workflowLinear.js +8 -6
  276. package/dist/support/workflowLinear.js.map +1 -1
  277. package/dist/support/worktreeManager.d.ts.map +1 -1
  278. package/dist/support/worktreeManager.js +33 -9
  279. package/dist/support/worktreeManager.js.map +1 -1
  280. package/dist/taskState/store.d.ts +7 -2
  281. package/dist/taskState/store.d.ts.map +1 -1
  282. package/dist/taskState/store.js +77 -21
  283. package/dist/taskState/store.js.map +1 -1
  284. package/dist/telemetry/telemetry.d.ts.map +1 -1
  285. package/dist/telemetry/telemetry.js +18 -4
  286. package/dist/telemetry/telemetry.js.map +1 -1
  287. package/dist/tui/components/ChatInput.d.ts +1 -0
  288. package/dist/tui/components/ChatInput.d.ts.map +1 -1
  289. package/dist/tui/components/ChatInput.js +14 -1
  290. package/dist/tui/components/ChatInput.js.map +1 -1
  291. package/dist/tui/components/SubagentTree.d.ts.map +1 -1
  292. package/dist/tui/components/SubagentTree.js +14 -2
  293. package/dist/tui/components/SubagentTree.js.map +1 -1
  294. package/dist/tui/hooks/useMonitor.d.ts.map +1 -1
  295. package/dist/tui/hooks/useMonitor.js +5 -0
  296. package/dist/tui/hooks/useMonitor.js.map +1 -1
  297. package/dist/tui/hooks/usePipelineEvents.d.ts.map +1 -1
  298. package/dist/tui/hooks/usePipelineEvents.js +12 -2
  299. package/dist/tui/hooks/usePipelineEvents.js.map +1 -1
  300. package/dist/tui/markdown.d.ts.map +1 -1
  301. package/dist/tui/markdown.js +11 -3
  302. package/dist/tui/markdown.js.map +1 -1
  303. package/dist/tui/monitorApi.d.ts.map +1 -1
  304. package/dist/tui/monitorApi.js +21 -0
  305. package/dist/tui/monitorApi.js.map +1 -1
  306. package/dist/tui/monitorRows.d.ts +2 -2
  307. package/dist/tui/monitorRows.d.ts.map +1 -1
  308. package/dist/tui/monitorRows.js +7 -1
  309. package/dist/tui/monitorRows.js.map +1 -1
  310. package/dist/tui/panels/ChatPanel.d.ts.map +1 -1
  311. package/dist/tui/panels/ChatPanel.js +30 -3
  312. package/dist/tui/panels/ChatPanel.js.map +1 -1
  313. package/dist/tui/sse.d.ts +1 -0
  314. package/dist/tui/sse.d.ts.map +1 -1
  315. package/dist/tui/sse.js +6 -1
  316. package/dist/tui/sse.js.map +1 -1
  317. package/dist/tui/subagentTree.d.ts.map +1 -1
  318. package/dist/tui/subagentTree.js +16 -5
  319. package/dist/tui/subagentTree.js.map +1 -1
  320. package/dist/tui/tabs.d.ts.map +1 -1
  321. package/dist/tui/tabs.js +7 -1
  322. package/dist/tui/tabs.js.map +1 -1
  323. package/package.json +1 -1
@@ -5,21 +5,61 @@
5
5
  // ============================================
6
6
  import { getIssueStore } from '../sqliteStore.js';
7
7
  import { autoLinkMemories, enrichIssueContext } from '../memoryBridge.js';
8
+ const DEFAULT_ISSUE_LIMIT = 50;
9
+ const MAX_ISSUE_LIMIT = 200;
10
+ const DEFAULT_EVENT_LIMIT = 50;
11
+ const DEFAULT_RECENT_EVENT_LIMIT = 20;
12
+ const MAX_EVENT_LIMIT = 200;
13
+ function clampLimit(limit, defaultLimit, maxLimit) {
14
+ if (limit === undefined || !Number.isInteger(limit))
15
+ return defaultLimit;
16
+ return Math.min(Math.max(limit, 1), maxLimit);
17
+ }
18
+ function clampOffset(offset) {
19
+ if (offset === undefined || !Number.isInteger(offset))
20
+ return 0;
21
+ return Math.max(offset, 0);
22
+ }
23
+ function sanitizeSearch(search) {
24
+ const normalized = search
25
+ ?.split('')
26
+ .map((char) => {
27
+ const code = char.charCodeAt(0);
28
+ return code < 32 || code === 127 ? ' ' : char;
29
+ })
30
+ .join('')
31
+ .trim()
32
+ .replace(/\s+/g, ' ');
33
+ if (!normalized)
34
+ return undefined;
35
+ return normalized
36
+ .split(' ')
37
+ .map((term) => `"${term.replace(/"/g, '""')}"`)
38
+ .join(' AND ');
39
+ }
40
+ function normalizeIssueFilter(filter) {
41
+ return {
42
+ ...filter,
43
+ search: sanitizeSearch(filter?.search),
44
+ limit: clampLimit(filter?.limit, DEFAULT_ISSUE_LIMIT, MAX_ISSUE_LIMIT),
45
+ offset: clampOffset(filter?.offset),
46
+ };
47
+ }
8
48
  export const resolvers = {
9
49
  Query: {
10
50
  issue: (_, { id }) => {
11
51
  return getIssueStore().getIssue(id);
12
52
  },
13
53
  issues: (_, { filter }) => {
14
- return getIssueStore().listIssues(filter);
54
+ return getIssueStore().listIssues(normalizeIssueFilter(filter));
15
55
  },
16
56
  labels: () => getIssueStore().listLabels(),
17
57
  milestones: () => getIssueStore().listMilestones(),
18
58
  issueEvents: (_, { issueId, limit }) => {
19
- return getIssueStore().getEvents(issueId, limit);
59
+ return getIssueStore().getEvents(issueId, clampLimit(limit, DEFAULT_EVENT_LIMIT, MAX_EVENT_LIMIT));
20
60
  },
21
61
  recentEvents: (_, { limit }) => {
22
- return getIssueStore().getRecentEvents(limit);
62
+ return getIssueStore().getRecentEvents(clampLimit(limit, DEFAULT_RECENT_EVENT_LIMIT, MAX_EVENT_LIMIT));
23
63
  },
24
64
  issueStats: (_, { projectId }) => {
25
65
  const stats = getIssueStore().getStats(projectId);
@@ -1 +1 @@
1
- {"version":3,"file":"resolvers.js","sourceRoot":"","sources":["../../../src/issues/graphql/resolvers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"resolvers.js","sourceRoot":"","sources":["../../../src/issues/graphql/resolvers.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,8CAA8C;AAC9C,sBAAsB;AACtB,gCAAgC;AAChC,+CAA+C;AAE/C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAG1E,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAC/B,MAAM,eAAe,GAAG,GAAG,CAAC;AAC5B,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAC/B,MAAM,0BAA0B,GAAG,EAAE,CAAC;AACtC,MAAM,eAAe,GAAG,GAAG,CAAC;AAE5B,SAAS,UAAU,CAAC,KAAyB,EAAE,YAAoB,EAAE,QAAgB;IACnF,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC;QAAE,OAAO,YAAY,CAAC;IACzE,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AAChD,CAAC;AAED,SAAS,WAAW,CAAC,MAA0B;IAC7C,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;QAAE,OAAO,CAAC,CAAC;IAChE,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,cAAc,CAAC,MAA0B;IAChD,MAAM,UAAU,GAAG,MAAM;QACvB,EAAE,KAAK,CAAC,EAAE,CAAC;SACV,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACZ,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAChC,OAAO,IAAI,GAAG,EAAE,IAAI,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IAChD,CAAC,CAAC;SACD,IAAI,CAAC,EAAE,CAAC;SACR,IAAI,EAAE;SACN,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACxB,IAAI,CAAC,UAAU;QAAE,OAAO,SAAS,CAAC;IAElC,OAAO,UAAU;SACd,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC;SAC9C,IAAI,CAAC,OAAO,CAAC,CAAC;AACnB,CAAC;AAED,SAAS,oBAAoB,CAAC,MAA+B;IAC3D,OAAO;QACL,GAAG,MAAM;QACT,MAAM,EAAE,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC;QACtC,KAAK,EAAE,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,mBAAmB,EAAE,eAAe,CAAC;QACtE,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC;KACpC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,KAAK,EAAE;QACL,KAAK,EAAE,CAAC,CAAU,EAAE,EAAE,EAAE,EAAkB,EAAE,EAAE;YAC5C,OAAO,aAAa,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACtC,CAAC;QAED,MAAM,EAAE,CAAC,CAAU,EAAE,EAAE,MAAM,EAA4B,EAAE,EAAE;YAC3D,OAAO,aAAa,EAAE,CAAC,UAAU,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC;QAClE,CAAC;QAED,MAAM,EAAE,GAAG,EAAE,CAAC,aAAa,EAAE,CAAC,UAAU,EAAE;QAC1C,UAAU,EAAE,GAAG,EAAE,CAAC,aAAa,EAAE,CAAC,cAAc,EAAE;QAElD,WAAW,EAAE,CAAC,CAAU,EAAE,EAAE,OAAO,EAAE,KAAK,EAAuC,EAAE,EAAE;YACnF,OAAO,aAAa,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,UAAU,CAAC,KAAK,EAAE,mBAAmB,EAAE,eAAe,CAAC,CAAC,CAAC;QACrG,CAAC;QAED,YAAY,EAAE,CAAC,CAAU,EAAE,EAAE,KAAK,EAAsB,EAAE,EAAE;YAC1D,OAAO,aAAa,EAAE,CAAC,eAAe,CAAC,UAAU,CAAC,KAAK,EAAE,0BAA0B,EAAE,eAAe,CAAC,CAAC,CAAC;QACzG,CAAC;QAED,UAAU,EAAE,CAAC,CAAU,EAAE,EAAE,SAAS,EAA0B,EAAE,EAAE;YAChE,MAAM,KAAK,GAAG,aAAa,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YAClD,OAAO;gBACL,GAAG,KAAK;gBACR,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;gBACtF,UAAU,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC9F,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;aAC/F,CAAC;QACJ,CAAC;QAED,cAAc,EAAE,CAAC,CAAU,EAAE,EAAE,OAAO,EAAuB,EAAE,EAAE;YAC/D,OAAO,aAAa,EAAE,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;QACpD,CAAC;QAED,YAAY,EAAE,KAAK,EAAE,CAAU,EAAE,EAAE,OAAO,EAAuB,EAAE,EAAE;YACnE,MAAM,KAAK,GAAG,aAAa,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YACtC,IAAI,CAAC,KAAK;gBAAE,MAAM,IAAI,KAAK,CAAC,SAAS,OAAO,YAAY,CAAC,CAAC;YAC1D,OAAO,kBAAkB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC1C,CAAC;KACF;IAED,QAAQ,EAAE;QACR,WAAW,EAAE,KAAK,EAAE,CAAU,EAAE,EAAE,KAAK,EAAkB,EAAE,EAAE;YAC3D,MAAM,KAAK,GAAG,aAAa,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;YAEvC,kCAAkC;YAClC,gBAAgB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBAC3C,OAAO,CAAC,IAAI,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;YAC/C,CAAC,CAAC,CAAC;YAEH,OAAO,KAAK,CAAC;QACf,CAAC;QAED,WAAW,EAAE,CAAC,CAAU,EAAE,EAAE,EAAE,EAAE,KAAK,EAA8B,EAAE,EAAE;YACrE,OAAO,aAAa,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QAChD,CAAC;QAED,WAAW,EAAE,CAAC,CAAU,EAAE,EAAE,EAAE,EAAkB,EAAE,EAAE;YAClD,OAAO,aAAa,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACzC,CAAC;QAED,iBAAiB,EAAE,CAAC,CAAU,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAA+C,EAAE,EAAE;YACpG,OAAO,aAAa,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;QACzD,CAAC;QAED,UAAU,EAAE,CAAC,CAAU,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAwD,EAAE,EAAE;YAC5G,OAAO,aAAa,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5E,CAAC;QAED,WAAW,EAAE,CAAC,CAAU,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAA0D,EAAE,EAAE;YAChH,OAAO,aAAa,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,IAAI,SAAS,EAAE,WAAW,IAAI,SAAS,CAAC,CAAC;QACzF,CAAC;QAED,WAAW,EAAE,CAAC,CAAU,EAAE,EAAE,EAAE,EAAkB,EAAE,EAAE;YAClD,OAAO,aAAa,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACzC,CAAC;QAED,eAAe,EAAE,CAAC,CAAU,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAA4D,EAAE,EAAE;YACxH,OAAO,aAAa,EAAE,CAAC,eAAe,CAAC,IAAI,EAAE,WAAW,IAAI,SAAS,EAAE,OAAO,IAAI,SAAS,CAAC,CAAC;QAC/F,CAAC;QAED,UAAU,EAAE,CAAC,CAAU,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAyC,EAAE,EAAE;YACvF,aAAa,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAC9C,OAAO,IAAI,CAAC;QACd,CAAC;QAED,gBAAgB,EAAE,KAAK,EAAE,CAAU,EAAE,EAAE,OAAO,EAAuB,EAAE,EAAE;YACvE,MAAM,KAAK,GAAG,aAAa,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YACtC,IAAI,CAAC,KAAK;gBAAE,MAAM,IAAI,KAAK,CAAC,SAAS,OAAO,YAAY,CAAC,CAAC;YAC1D,OAAO,gBAAgB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACxC,CAAC;KACF;CACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/issues/graphql/server.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGjE,QAAA,MAAM,IAAI,mDAiBR,CAAC;AAEH;;;GAGG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,IAAI,CAAC,CAaf;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAGjE;AAED,OAAO,EAAE,IAAI,EAAE,CAAC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/issues/graphql/server.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AA8EjE,QAAA,MAAM,IAAI,mDAcR,CAAC;AAEH;;;GAGG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,IAAI,CAAC,CAef;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAGjE;AAED,OAAO,EAAE,IAAI,EAAE,CAAC"}
@@ -3,11 +3,83 @@
3
3
  // Created: 2026-04-03
4
4
  // Purpose: graphql-yoga 서버, 기존 HTTP 서버에 통합
5
5
  // ============================================
6
+ import { GraphQLError, getOperationAST } from 'graphql';
6
7
  import { createSchema, createYoga } from 'graphql-yoga';
7
8
  import { typeDefs } from './typeDefs.js';
8
9
  import { resolvers } from './resolvers.js';
9
10
  import { registryTypeDefs } from '../../registry/graphql/typeDefs.js';
10
11
  import { registryResolvers } from '../../registry/graphql/resolvers.js';
12
+ const CORS_METHODS = 'GET, POST, OPTIONS';
13
+ const CORS_HEADERS = 'Content-Type, Authorization, X-OpenSwarm-GraphQL-Token';
14
+ function isAllowedOrigin(origin) {
15
+ let url;
16
+ try {
17
+ url = new URL(origin);
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ const { protocol, hostname } = url;
23
+ if (protocol !== 'http:' && protocol !== 'https:')
24
+ return false;
25
+ if (hostname === 'localhost' || hostname === '127.0.0.1')
26
+ return true;
27
+ if (hostname === 'tauri.localhost')
28
+ return true;
29
+ const tailscaleMatch = hostname.match(/^100\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/);
30
+ if (!tailscaleMatch)
31
+ return false;
32
+ const second = Number(tailscaleMatch[1]);
33
+ return second >= 64 && second <= 127;
34
+ }
35
+ function applyCors(req, res) {
36
+ const origin = req.headers.origin;
37
+ if (origin && isAllowedOrigin(origin)) {
38
+ res.setHeader('Access-Control-Allow-Origin', origin);
39
+ res.setHeader('Vary', 'Origin');
40
+ res.setHeader('Access-Control-Allow-Methods', CORS_METHODS);
41
+ res.setHeader('Access-Control-Allow-Headers', CORS_HEADERS);
42
+ }
43
+ if (req.method !== 'OPTIONS')
44
+ return false;
45
+ res.writeHead(origin && !isAllowedOrigin(origin) ? 403 : 204);
46
+ res.end();
47
+ return true;
48
+ }
49
+ function hasValidMutationToken(request) {
50
+ const token = process.env.OPENSWARM_GRAPHQL_TOKEN?.trim();
51
+ if (!token)
52
+ return false;
53
+ const auth = request.headers.get('authorization') ?? '';
54
+ const bearer = auth.match(/^Bearer\s+(.+)$/i)?.[1]?.trim();
55
+ const headerToken = request.headers.get('x-openswarm-graphql-token')?.trim();
56
+ return bearer === token || headerToken === token;
57
+ }
58
+ function isMutationRequestAuthorized(request) {
59
+ if (hasValidMutationToken(request))
60
+ return true;
61
+ const origin = request.headers.get('origin');
62
+ return origin ? isAllowedOrigin(origin) : true;
63
+ }
64
+ const mutationAuthPlugin = {
65
+ onExecute({ args, setResultAndStopExecution }) {
66
+ const operation = getOperationAST(args.document, args.operationName);
67
+ if (operation?.operation !== 'mutation')
68
+ return;
69
+ if (isMutationRequestAuthorized(args.contextValue.request))
70
+ return;
71
+ setResultAndStopExecution({
72
+ errors: [
73
+ new GraphQLError('Unauthorized GraphQL mutation', {
74
+ extensions: {
75
+ code: 'UNAUTHORIZED',
76
+ http: { status: 403 },
77
+ },
78
+ }),
79
+ ],
80
+ });
81
+ },
82
+ };
11
83
  // GraphQL Yoga 인스턴스 생성 (이슈 + 코드 레지스트리 스키마 머지)
12
84
  const yoga = createYoga({
13
85
  schema: createSchema({
@@ -15,11 +87,8 @@ const yoga = createYoga({
15
87
  resolvers: [resolvers, registryResolvers],
16
88
  }),
17
89
  graphqlEndpoint: '/graphql',
18
- // CORS 허용 (대시보드에서 접근)
19
- cors: {
20
- origin: '*',
21
- methods: ['GET', 'POST', 'OPTIONS'],
22
- },
90
+ cors: false,
91
+ plugins: [mutationAuthPlugin],
23
92
  logging: {
24
93
  debug: () => { },
25
94
  info: (...args) => console.log('[GraphQL]', ...args),
@@ -32,6 +101,8 @@ const yoga = createYoga({
32
101
  * graphql-yoga의 Node.js HTTP adapter 패턴 사용
33
102
  */
34
103
  export async function handleGraphQL(req, res) {
104
+ if (applyCors(req, res))
105
+ return;
35
106
  // graphql-yoga는 Node.js HTTP 서버에서 req, res를 직접 처리
36
107
  // handle() 호출 시 내부적으로 res에 응답을 작성
37
108
  const response = await yoga.handle(req, res);
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../../../src/issues/graphql/server.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,2CAA2C;AAC3C,sBAAsB;AACtB,2CAA2C;AAC3C,+CAA+C;AAE/C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AAGxE,8CAA8C;AAC9C,MAAM,IAAI,GAAG,UAAU,CAAC;IACtB,MAAM,EAAE,YAAY,CAAC;QACnB,QAAQ,EAAE,CAAC,QAAQ,EAAE,gBAAgB,CAAC;QACtC,SAAS,EAAE,CAAC,SAAS,EAAE,iBAAiB,CAAC;KAC1C,CAAC;IACF,eAAe,EAAE,UAAU;IAC3B,sBAAsB;IACtB,IAAI,EAAE;QACJ,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC;KACpC;IACD,OAAO,EAAE;QACP,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;QACf,IAAI,EAAE,CAAC,GAAG,IAAW,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC;QAC3D,IAAI,EAAE,CAAC,GAAG,IAAW,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC;QAC5D,KAAK,EAAE,CAAC,GAAG,IAAW,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC;KAC/D;CACF,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,GAAoB,EACpB,GAAmB;IAEnB,kDAAkD;IAClD,kCAAkC;IAClC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAY,CAAC;IAExD,mCAAmC;IACnC,IAAI,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,IAAI,QAAQ,EAAE,CAAC;QACrE,MAAM,CAAC,GAAG,QAAoB,CAAC;QAC/B,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QACjE,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5B,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChB,CAAC;IACD,iCAAiC;AACnC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAuB;IACtD,IAAI,CAAC,GAAG;QAAE,OAAO,KAAK,CAAC;IACvB,OAAO,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;AACpC,CAAC;AAED,OAAO,EAAE,IAAI,EAAE,CAAC"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../../src/issues/graphql/server.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- export declare const typeDefs = "\n type Query {\n # \uC774\uC288\n issue(id: ID!): Issue\n issues(filter: IssueFilterInput): IssueConnection!\n\n # \uB77C\uBCA8 & \uB9C8\uC77C\uC2A4\uD1A4\n labels: [Label!]!\n milestones: [Milestone!]!\n\n # \uC774\uBCA4\uD2B8 \uB85C\uADF8\n issueEvents(issueId: ID!, limit: Int): [IssueEvent!]!\n recentEvents(limit: Int): [IssueEvent!]!\n\n # \uD1B5\uACC4\n issueStats(projectId: String): IssueStats!\n\n # \uBA54\uBAA8\uB9AC \uC5F0\uB3D9\n linkedMemories(issueId: ID!): [String!]!\n issueContext(issueId: ID!): IssueContext!\n }\n\n type Mutation {\n # \uC774\uC288 CRUD\n createIssue(input: CreateIssueInput!): Issue!\n updateIssue(id: ID!, input: UpdateIssueInput!): Issue\n deleteIssue(id: ID!): Boolean!\n\n # \uC0C1\uD0DC \uC804\uC774\n changeIssueStatus(id: ID!, status: IssueStatus!, actor: String): Issue\n\n # \uC774\uBCA4\uD2B8\n addComment(issueId: ID!, content: String!, actor: String): IssueEvent!\n\n # \uB77C\uBCA8\n createLabel(name: String!, color: String, description: String): Label!\n deleteLabel(id: ID!): Boolean!\n\n # \uB9C8\uC77C\uC2A4\uD1A4\n createMilestone(name: String!, description: String, dueDate: String): Milestone!\n\n # \uBA54\uBAA8\uB9AC \uC5F0\uB3D9\n linkMemory(issueId: ID!, memoryId: String!): Boolean!\n autoLinkMemories(issueId: ID!): [String!]!\n }\n\n type Subscription {\n issueUpdated(projectId: String): Issue!\n issueEventAdded(issueId: ID): IssueEvent!\n }\n\n # ---- Types ----\n\n type Issue {\n id: ID!\n projectId: String!\n title: String!\n description: String!\n status: IssueStatus!\n priority: IssuePriority!\n source: IssueSource!\n labels: [String!]!\n assignee: String\n milestone: String\n relevantFiles: [String!]!\n acceptanceCriteria: [String!]!\n estimateMinutes: Int\n complexity: Complexity\n dependencies: [String!]!\n parentId: String\n childIds: [String!]!\n linearId: String\n linearIdentifier: String\n linearUrl: String\n memoryIds: [String!]!\n createdAt: String!\n updatedAt: String!\n closedAt: String\n }\n\n type IssueConnection {\n issues: [Issue!]!\n total: Int!\n }\n\n type IssueEvent {\n id: ID!\n issueId: String!\n type: IssueEventType!\n oldValue: String\n newValue: String\n content: String\n memoryId: String\n actor: String!\n createdAt: String!\n }\n\n type Label {\n id: ID!\n name: String!\n color: String!\n description: String\n }\n\n type Milestone {\n id: ID!\n name: String!\n description: String\n dueDate: String\n status: String!\n createdAt: String!\n }\n\n type IssueStats {\n total: Int!\n byStatus: [StatusCount!]!\n byPriority: [PriorityCount!]!\n byProject: [ProjectCount!]!\n recentlyCreated: Int!\n recentlyClosed: Int!\n }\n\n type StatusCount {\n status: String!\n count: Int!\n }\n\n type PriorityCount {\n priority: String!\n count: Int!\n }\n\n type ProjectCount {\n projectId: String!\n count: Int!\n }\n\n type IssueContext {\n linkedMemories: [MemoryRef!]!\n similarIssues: [Issue!]!\n }\n\n type MemoryRef {\n id: String!\n content: String!\n score: Float!\n }\n\n # ---- Enums ----\n\n enum IssueStatus {\n backlog\n todo\n in_progress\n in_review\n done\n cancelled\n }\n\n enum IssuePriority {\n urgent\n high\n medium\n low\n none\n }\n\n enum IssueSource {\n local\n linear\n github\n discord\n }\n\n enum Complexity {\n simple\n moderate\n complex\n very_complex\n }\n\n enum IssueEventType {\n created\n status_changed\n priority_changed\n assigned\n commented\n labeled\n linked\n memory_linked\n closed\n reopened\n }\n\n # ---- Inputs ----\n\n input CreateIssueInput {\n projectId: String!\n title: String!\n description: String\n status: IssueStatus\n priority: IssuePriority\n source: IssueSource\n labels: [String!]\n assignee: String\n milestone: String\n relevantFiles: [String!]\n acceptanceCriteria: [String!]\n estimateMinutes: Int\n complexity: Complexity\n dependencies: [String!]\n parentId: String\n linearId: String\n linearIdentifier: String\n linearUrl: String\n }\n\n input UpdateIssueInput {\n title: String\n description: String\n priority: IssuePriority\n labels: [String!]\n assignee: String\n milestone: String\n relevantFiles: [String!]\n acceptanceCriteria: [String!]\n estimateMinutes: Int\n complexity: Complexity\n dependencies: [String!]\n parentId: String\n }\n\n input IssueFilterInput {\n projectId: String\n status: [IssueStatus!]\n priority: [IssuePriority!]\n labels: [String!]\n assignee: String\n source: IssueSource\n parentId: String\n search: String\n limit: Int\n offset: Int\n }\n";
1
+ export declare const typeDefs = "\n type Query {\n # \uC774\uC288\n issue(id: ID!): Issue\n issues(filter: IssueFilterInput): IssueConnection!\n\n # \uB77C\uBCA8 & \uB9C8\uC77C\uC2A4\uD1A4\n labels: [Label!]!\n milestones: [Milestone!]!\n\n # \uC774\uBCA4\uD2B8 \uB85C\uADF8\n issueEvents(issueId: ID!, limit: Int): [IssueEvent!]!\n recentEvents(limit: Int): [IssueEvent!]!\n\n # \uD1B5\uACC4\n issueStats(projectId: String): IssueStats!\n\n # \uBA54\uBAA8\uB9AC \uC5F0\uB3D9\n linkedMemories(issueId: ID!): [String!]!\n issueContext(issueId: ID!): IssueContext!\n }\n\n type Mutation {\n # \uC774\uC288 CRUD\n createIssue(input: CreateIssueInput!): Issue!\n updateIssue(id: ID!, input: UpdateIssueInput!): Issue\n deleteIssue(id: ID!): Boolean!\n\n # \uC0C1\uD0DC \uC804\uC774\n changeIssueStatus(id: ID!, status: IssueStatus!, actor: String): Issue\n\n # \uC774\uBCA4\uD2B8\n addComment(issueId: ID!, content: String!, actor: String): IssueEvent!\n\n # \uB77C\uBCA8\n createLabel(name: String!, color: String, description: String): Label!\n deleteLabel(id: ID!): Boolean!\n\n # \uB9C8\uC77C\uC2A4\uD1A4\n createMilestone(name: String!, description: String, dueDate: String): Milestone!\n\n # \uBA54\uBAA8\uB9AC \uC5F0\uB3D9\n linkMemory(issueId: ID!, memoryId: String!): Boolean!\n autoLinkMemories(issueId: ID!): [String!]!\n }\n\n # ---- Types ----\n\n type Issue {\n id: ID!\n projectId: String!\n title: String!\n description: String!\n status: IssueStatus!\n priority: IssuePriority!\n source: IssueSource!\n labels: [String!]!\n assignee: String\n milestone: String\n relevantFiles: [String!]!\n acceptanceCriteria: [String!]!\n estimateMinutes: Int\n complexity: Complexity\n dependencies: [String!]!\n parentId: String\n childIds: [String!]!\n linearId: String\n linearIdentifier: String\n linearUrl: String\n memoryIds: [String!]!\n createdAt: String!\n updatedAt: String!\n closedAt: String\n }\n\n type IssueConnection {\n issues: [Issue!]!\n total: Int!\n }\n\n type IssueEvent {\n id: ID!\n issueId: String!\n type: IssueEventType!\n oldValue: String\n newValue: String\n content: String\n memoryId: String\n actor: String!\n createdAt: String!\n }\n\n type Label {\n id: ID!\n name: String!\n color: String!\n description: String\n }\n\n type Milestone {\n id: ID!\n name: String!\n description: String\n dueDate: String\n status: String!\n createdAt: String!\n }\n\n type IssueStats {\n total: Int!\n byStatus: [StatusCount!]!\n byPriority: [PriorityCount!]!\n byProject: [ProjectCount!]!\n recentlyCreated: Int!\n recentlyClosed: Int!\n }\n\n type StatusCount {\n status: String!\n count: Int!\n }\n\n type PriorityCount {\n priority: String!\n count: Int!\n }\n\n type ProjectCount {\n projectId: String!\n count: Int!\n }\n\n type IssueContext {\n linkedMemories: [MemoryRef!]!\n similarIssues: [Issue!]!\n }\n\n type MemoryRef {\n id: String!\n content: String!\n score: Float!\n }\n\n # ---- Enums ----\n\n enum IssueStatus {\n backlog\n todo\n in_progress\n in_review\n done\n cancelled\n }\n\n enum IssuePriority {\n urgent\n high\n medium\n low\n none\n }\n\n enum IssueSource {\n local\n linear\n github\n discord\n }\n\n enum Complexity {\n simple\n moderate\n complex\n very_complex\n }\n\n enum IssueEventType {\n created\n status_changed\n priority_changed\n assigned\n commented\n labeled\n linked\n memory_linked\n closed\n reopened\n }\n\n # ---- Inputs ----\n\n input CreateIssueInput {\n projectId: String!\n title: String!\n description: String\n status: IssueStatus\n priority: IssuePriority\n source: IssueSource\n labels: [String!]\n assignee: String\n milestone: String\n relevantFiles: [String!]\n acceptanceCriteria: [String!]\n estimateMinutes: Int\n complexity: Complexity\n dependencies: [String!]\n parentId: String\n linearId: String\n linearIdentifier: String\n linearUrl: String\n }\n\n input UpdateIssueInput {\n title: String\n description: String\n priority: IssuePriority\n labels: [String!]\n assignee: String\n milestone: String\n relevantFiles: [String!]\n acceptanceCriteria: [String!]\n estimateMinutes: Int\n complexity: Complexity\n dependencies: [String!]\n parentId: String\n }\n\n input IssueFilterInput {\n projectId: String\n status: [IssueStatus!]\n priority: [IssuePriority!]\n labels: [String!]\n assignee: String\n source: IssueSource\n parentId: String\n search: String\n limit: Int\n offset: Int\n }\n";
2
2
  //# sourceMappingURL=typeDefs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"typeDefs.d.ts","sourceRoot":"","sources":["../../../src/issues/graphql/typeDefs.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,QAAQ,k4JAoPpB,CAAC"}
1
+ {"version":3,"file":"typeDefs.d.ts","sourceRoot":"","sources":["../../../src/issues/graphql/typeDefs.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,QAAQ,wwJA+OpB,CAAC"}
@@ -49,11 +49,6 @@ export const typeDefs = /* GraphQL */ `
49
49
  autoLinkMemories(issueId: ID!): [String!]!
50
50
  }
51
51
 
52
- type Subscription {
53
- issueUpdated(projectId: String): Issue!
54
- issueEventAdded(issueId: ID): IssueEvent!
55
- }
56
-
57
52
  # ---- Types ----
58
53
 
59
54
  type Issue {
@@ -1 +1 @@
1
- {"version":3,"file":"typeDefs.js","sourceRoot":"","sources":["../../../src/issues/graphql/typeDefs.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,qDAAqD;AACrD,sBAAsB;AACtB,0BAA0B;AAC1B,+CAA+C;AAE/C,MAAM,CAAC,MAAM,QAAQ,GAAG,aAAa,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoPrC,CAAC"}
1
+ {"version":3,"file":"typeDefs.js","sourceRoot":"","sources":["../../../src/issues/graphql/typeDefs.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,qDAAqD;AACrD,sBAAsB;AACtB,0BAA0B;AAC1B,+CAA+C;AAE/C,MAAM,CAAC,MAAM,QAAQ,GAAG,aAAa,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+OrC,CAAC"}
@@ -1,2 +1,2 @@
1
- export declare const ISSUE_BOARD_HTML = "<!DOCTYPE html>\n<html lang=\"ko\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>OpenSwarm :: Issues</title>\n <style>\n :root {\n --bg: #0a0c0a;\n --bg2: #0d100d;\n --bg3: #111411;\n --green: #00ff41;\n --green-dim: #003a00;\n --green-mid: #00aa00;\n --green-lo: #005500;\n --cyan: #00ccdd;\n --cyan-dim: #003344;\n --amber: #ffaa00;\n --red: #ff3333;\n --white: #ccddcc;\n --dim: #445544;\n --border: #1a2a1a;\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: 'Cascadia Code', 'JetBrains Mono', 'Fira Code', monospace;\n background: var(--bg);\n color: var(--white);\n font-size: 13px;\n line-height: 1.4;\n height: 100vh;\n display: flex;\n flex-direction: column;\n }\n\n /* Header */\n header {\n height: 38px;\n background: var(--bg2);\n border-bottom: 1px solid var(--border);\n display: flex;\n align-items: center;\n padding: 0 1rem;\n gap: 0.75rem;\n flex-shrink: 0;\n }\n .hdr-logo { color: var(--green); font-weight: bold; font-size: 14px; letter-spacing: 0.15em; text-decoration: none; }\n .hdr-sep { color: var(--dim); margin: 0 0.5rem; }\n .hdr-sub { color: var(--cyan); font-size: 12px; letter-spacing: 0.1em; }\n .hdr-right { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }\n\n /* Toolbar */\n .toolbar {\n background: var(--bg2);\n border-bottom: 1px solid var(--border);\n padding: 6px 1rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n flex-shrink: 0;\n }\n .btn {\n background: var(--bg3);\n color: var(--green);\n border: 1px solid var(--green-dim);\n padding: 3px 10px;\n font-family: inherit;\n font-size: 11px;\n cursor: pointer;\n border-radius: 3px;\n }\n .btn:hover { background: var(--green-dim); }\n .btn-primary { color: var(--cyan); border-color: var(--cyan-dim); }\n .btn-primary:hover { background: var(--cyan-dim); }\n .filter-select {\n background: var(--bg3);\n color: var(--white);\n border: 1px solid var(--border);\n padding: 3px 6px;\n font-family: inherit;\n font-size: 11px;\n border-radius: 3px;\n }\n .search-input {\n background: var(--bg3);\n color: var(--white);\n border: 1px solid var(--border);\n padding: 3px 8px;\n font-family: inherit;\n font-size: 11px;\n width: 200px;\n border-radius: 3px;\n }\n .search-input::placeholder { color: var(--dim); }\n .stats-bar { margin-left: auto; color: var(--dim); font-size: 11px; }\n .stats-bar span { margin: 0 0.5rem; }\n .stats-val { color: var(--green); }\n\n /* Kanban Board */\n .board {\n flex: 1;\n display: flex;\n gap: 2px;\n padding: 8px;\n overflow-x: auto;\n min-height: 0;\n }\n .column {\n flex: 1;\n min-width: 220px;\n max-width: 320px;\n background: var(--bg2);\n border: 1px solid var(--border);\n border-radius: 4px;\n display: flex;\n flex-direction: column;\n }\n .col-header {\n padding: 6px 10px;\n background: var(--bg3);\n border-bottom: 1px solid var(--border);\n font-size: 11px;\n letter-spacing: 0.1em;\n display: flex;\n align-items: center;\n gap: 6px;\n }\n .col-header .count {\n background: var(--green-dim);\n color: var(--green);\n padding: 1px 6px;\n border-radius: 8px;\n font-size: 10px;\n }\n .col-body {\n flex: 1;\n overflow-y: auto;\n padding: 4px;\n }\n\n /* Issue Card */\n .card {\n background: var(--bg3);\n border: 1px solid var(--border);\n border-radius: 3px;\n padding: 8px;\n margin-bottom: 4px;\n cursor: pointer;\n transition: border-color 0.15s;\n }\n .card:hover { border-color: var(--green-lo); }\n .card-title { font-size: 12px; color: var(--white); margin-bottom: 4px; }\n .card-meta { font-size: 10px; color: var(--dim); display: flex; gap: 6px; flex-wrap: wrap; }\n .card-priority {\n display: inline-block;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n margin-right: 4px;\n }\n .p-urgent { background: var(--red); }\n .p-high { background: var(--amber); }\n .p-medium { background: var(--cyan); }\n .p-low { background: var(--dim); }\n .p-none { background: transparent; border: 1px solid var(--dim); }\n .card-label {\n font-size: 9px;\n padding: 1px 4px;\n border-radius: 2px;\n background: var(--green-dim);\n color: var(--green-mid);\n }\n .card-id { color: var(--dim); font-size: 9px; }\n\n /* Modal */\n .modal-overlay {\n display: none;\n position: fixed;\n inset: 0;\n background: rgba(0,0,0,0.7);\n z-index: 100;\n justify-content: center;\n align-items: center;\n }\n .modal-overlay.active { display: flex; }\n .modal {\n background: var(--bg2);\n border: 1px solid var(--green-dim);\n border-radius: 6px;\n width: 90%;\n max-width: 600px;\n max-height: 85vh;\n overflow-y: auto;\n padding: 1.5rem;\n }\n .modal h3 { color: var(--green); font-size: 14px; margin-bottom: 1rem; letter-spacing: 0.1em; }\n .form-group { margin-bottom: 0.75rem; }\n .form-group label { display: block; color: var(--dim); font-size: 10px; margin-bottom: 3px; letter-spacing: 0.05em; }\n .form-group input, .form-group textarea, .form-group select {\n width: 100%;\n background: var(--bg3);\n color: var(--white);\n border: 1px solid var(--border);\n padding: 6px 8px;\n font-family: inherit;\n font-size: 12px;\n border-radius: 3px;\n }\n .form-group textarea { min-height: 80px; resize: vertical; }\n .form-actions { display: flex; gap: 0.5rem; margin-top: 1rem; justify-content: flex-end; }\n .form-actions .btn { padding: 5px 16px; }\n\n /* Detail panel */\n .detail-panel {\n display: none;\n position: fixed;\n top: 0;\n right: 0;\n width: 420px;\n height: 100%;\n background: var(--bg2);\n border-left: 1px solid var(--green-dim);\n z-index: 90;\n overflow-y: auto;\n padding: 1rem;\n }\n .detail-panel.active { display: block; }\n .detail-close { float: right; cursor: pointer; color: var(--dim); font-size: 16px; }\n .detail-title { color: var(--green); font-size: 14px; margin-bottom: 0.5rem; margin-right: 2rem; }\n .detail-section { margin-top: 1rem; }\n .detail-section h4 { color: var(--cyan); font-size: 11px; letter-spacing: 0.05em; margin-bottom: 0.25rem; }\n .detail-section p, .detail-section ul { color: var(--white); font-size: 12px; }\n .detail-section ul { padding-left: 1rem; }\n .event-item { font-size: 11px; color: var(--dim); padding: 3px 0; border-bottom: 1px solid var(--border); }\n .event-item .ev-type { color: var(--amber); }\n </style>\n</head>\n<body>\n <header>\n <a href=\"/\" class=\"hdr-logo\">OpenSwarm</a>\n <span class=\"hdr-sep\">::</span>\n <span class=\"hdr-sub\">ISSUE TRACKER</span>\n <div class=\"hdr-right\">\n <span id=\"stats-summary\" style=\"color:var(--dim);font-size:11px\"></span>\n </div>\n </header>\n\n <div class=\"toolbar\">\n <button class=\"btn btn-primary\" onclick=\"openCreateModal()\">+ NEW ISSUE</button>\n <select class=\"filter-select\" id=\"filter-project\" onchange=\"applyFilter()\">\n <option value=\"\">all projects</option>\n </select>\n <select class=\"filter-select\" id=\"filter-priority\" onchange=\"applyFilter()\">\n <option value=\"\">all priorities</option>\n <option value=\"urgent\">urgent</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n </select>\n <input type=\"text\" class=\"search-input\" id=\"search-input\" placeholder=\"search issues...\" oninput=\"debounceSearch()\">\n <div class=\"stats-bar\">\n <span>total: <span class=\"stats-val\" id=\"stat-total\">0</span></span>\n <span>open: <span class=\"stats-val\" id=\"stat-open\">0</span></span>\n <span>done: <span class=\"stats-val\" id=\"stat-done\">0</span></span>\n </div>\n </div>\n\n <div class=\"board\" id=\"board\">\n <!-- \uCE78\uBC18 \uCE7C\uB7FC\uC740 JS\uC5D0\uC11C \uB3D9\uC801 \uC0DD\uC131 -->\n </div>\n\n <!-- \uC774\uC288 \uC0DD\uC131 \uBAA8\uB2EC -->\n <div class=\"modal-overlay\" id=\"create-modal\">\n <div class=\"modal\">\n <h3>NEW ISSUE</h3>\n <div class=\"form-group\">\n <label>PROJECT</label>\n <select id=\"new-project\"></select>\n </div>\n <div class=\"form-group\">\n <label>TITLE</label>\n <input type=\"text\" id=\"new-title\" placeholder=\"\uC774\uC288 \uC81C\uBAA9\">\n </div>\n <div class=\"form-group\">\n <label>DESCRIPTION</label>\n <textarea id=\"new-desc\" placeholder=\"\uC0C1\uC138 \uC124\uBA85...\"></textarea>\n </div>\n <div style=\"display:flex;gap:0.75rem\">\n <div class=\"form-group\" style=\"flex:1\">\n <label>PRIORITY</label>\n <select id=\"new-priority\">\n <option value=\"medium\">medium</option>\n <option value=\"urgent\">urgent</option>\n <option value=\"high\">high</option>\n <option value=\"low\">low</option>\n <option value=\"none\">none</option>\n </select>\n </div>\n <div class=\"form-group\" style=\"flex:1\">\n <label>STATUS</label>\n <select id=\"new-status\">\n <option value=\"backlog\">backlog</option>\n <option value=\"todo\">todo</option>\n <option value=\"in_progress\">in_progress</option>\n </select>\n </div>\n </div>\n <div class=\"form-group\">\n <label>LABELS (comma separated)</label>\n <input type=\"text\" id=\"new-labels\" placeholder=\"bug, feature, ...\">\n </div>\n <div class=\"form-group\">\n <label>RELEVANT FILES (comma separated)</label>\n <input type=\"text\" id=\"new-files\" placeholder=\"src/foo.ts, src/bar.ts\">\n </div>\n <div class=\"form-actions\">\n <button class=\"btn\" onclick=\"closeCreateModal()\">CANCEL</button>\n <button class=\"btn btn-primary\" onclick=\"createIssue()\">CREATE</button>\n </div>\n </div>\n </div>\n\n <!-- \uC774\uC288 \uC0C1\uC138 \uD328\uB110 -->\n <div class=\"detail-panel\" id=\"detail-panel\">\n <span class=\"detail-close\" onclick=\"closeDetail()\">&times;</span>\n <div id=\"detail-content\"></div>\n </div>\n\n <script>\n const COLUMNS = [\n { status: 'backlog', label: 'BACKLOG', color: 'var(--dim)' },\n { status: 'todo', label: 'TODO', color: 'var(--white)' },\n { status: 'in_progress', label: 'IN PROGRESS', color: 'var(--amber)' },\n { status: 'in_review', label: 'IN REVIEW', color: 'var(--cyan)' },\n { status: 'done', label: 'DONE', color: 'var(--green)' },\n ];\n\n let allIssues = [];\n let projects = new Set();\n\n // GraphQL helper\n async function gql(query, variables = {}) {\n const res = await fetch('/graphql', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ query, variables }),\n });\n const json = await res.json();\n if (json.errors) {\n console.error('GraphQL errors:', json.errors);\n throw new Error(json.errors[0].message);\n }\n return json.data;\n }\n\n // \uC774\uC288 \uBAA9\uB85D \uB85C\uB4DC\n async function loadIssues() {\n const projectId = document.getElementById('filter-project').value || undefined;\n const priority = document.getElementById('filter-priority').value || undefined;\n const search = document.getElementById('search-input').value || undefined;\n\n const filter = {};\n if (projectId) filter.projectId = projectId;\n if (priority) filter.priority = [priority];\n if (search) filter.search = search;\n\n const data = await gql(`\n query ListIssues($filter: IssueFilterInput) {\n issues(filter: $filter) {\n issues {\n id projectId title description status priority source\n labels assignee relevantFiles dependencies childIds\n linearIdentifier memoryIds createdAt updatedAt closedAt\n }\n total\n }\n issueStats {\n total\n byStatus { status count }\n }\n }\n `, { filter: Object.keys(filter).length > 0 ? filter : null });\n\n allIssues = data.issues.issues;\n\n // \uD504\uB85C\uC81D\uD2B8 \uBAA9\uB85D \uAC31\uC2E0\n for (const iss of allIssues) projects.add(iss.projectId);\n updateProjectFilter();\n\n // \uD1B5\uACC4 \uAC31\uC2E0\n const stats = data.issueStats;\n document.getElementById('stat-total').textContent = stats.total;\n const openCount = stats.byStatus\n .filter(s => !['done', 'cancelled'].includes(s.status))\n .reduce((a, s) => a + s.count, 0);\n const doneCount = stats.byStatus.find(s => s.status === 'done')?.count || 0;\n document.getElementById('stat-open').textContent = openCount;\n document.getElementById('stat-done').textContent = doneCount;\n\n renderBoard();\n }\n\n function updateProjectFilter() {\n const sel = document.getElementById('filter-project');\n const current = sel.value;\n const opts = ['<option value=\"\">all projects</option>'];\n for (const p of projects) {\n opts.push('<option value=\"' + p + '\"' + (p === current ? ' selected' : '') + '>' + p + '</option>');\n }\n sel.innerHTML = opts.join('');\n }\n\n // \uCE78\uBC18 \uBCF4\uB4DC \uB80C\uB354\uB9C1\n function renderBoard() {\n const board = document.getElementById('board');\n board.innerHTML = '';\n\n for (const col of COLUMNS) {\n const issues = allIssues.filter(i => i.status === col.status);\n const colEl = document.createElement('div');\n colEl.className = 'column';\n colEl.innerHTML = `\n <div class=\"col-header\">\n <span style=\"color:${col.color}\">${col.label}</span>\n <span class=\"count\">${issues.length}</span>\n </div>\n <div class=\"col-body\" data-status=\"${col.status}\"></div>\n `;\n\n const body = colEl.querySelector('.col-body');\n for (const iss of issues) {\n body.appendChild(createCard(iss));\n }\n\n // \uB4DC\uB798\uADF8 \uB4DC\uB86D \uC218\uC2E0\n body.addEventListener('dragover', e => { e.preventDefault(); body.style.background = 'var(--green-dim)'; });\n body.addEventListener('dragleave', () => { body.style.background = ''; });\n body.addEventListener('drop', async e => {\n e.preventDefault();\n body.style.background = '';\n const issueId = e.dataTransfer.getData('text/plain');\n if (issueId && col.status) {\n await gql(`mutation($id:ID!,$s:IssueStatus!){changeIssueStatus(id:$id,status:$s){id}}`,\n { id: issueId, s: col.status });\n loadIssues();\n }\n });\n\n board.appendChild(colEl);\n }\n }\n\n function createCard(iss) {\n const card = document.createElement('div');\n card.className = 'card';\n card.draggable = true;\n card.addEventListener('dragstart', e => { e.dataTransfer.setData('text/plain', iss.id); });\n card.addEventListener('click', () => openDetail(iss.id));\n\n const priorityClass = 'p-' + iss.priority;\n const labels = (iss.labels || []).map(l => '<span class=\"card-label\">' + l + '</span>').join('');\n const timeAgo = formatTimeAgo(iss.updatedAt);\n\n card.innerHTML = `\n <div class=\"card-title\"><span class=\"card-priority ${priorityClass}\"></span>${escHtml(iss.title)}</div>\n <div class=\"card-meta\">\n <span class=\"card-id\">${iss.id.slice(0, 6)}</span>\n <span>${iss.projectId}</span>\n ${iss.assignee ? '<span>' + iss.assignee + '</span>' : ''}\n <span>${timeAgo}</span>\n </div>\n ${labels ? '<div class=\"card-meta\" style=\"margin-top:3px\">' + labels + '</div>' : ''}\n `;\n return card;\n }\n\n // \uC774\uC288 \uC0C1\uC138 \uD328\uB110\n async function openDetail(id) {\n const data = await gql(`\n query IssueDetail($id:ID!) {\n issue(id:$id) {\n id projectId title description status priority source\n labels assignee relevantFiles acceptanceCriteria\n dependencies childIds memoryIds createdAt updatedAt closedAt\n linearIdentifier linearUrl\n }\n issueEvents(issueId:$id, limit:20) {\n id type oldValue newValue content actor createdAt\n }\n }\n `, { id });\n\n const iss = data.issue;\n if (!iss) return;\n const events = data.issueEvents;\n\n const panel = document.getElementById('detail-panel');\n const content = document.getElementById('detail-content');\n\n const statusOptions = COLUMNS.map(c =>\n '<option value=\"' + c.status + '\"' + (c.status === iss.status ? ' selected' : '') + '>' + c.label + '</option>'\n ).join('');\n\n content.innerHTML = `\n <div class=\"detail-title\">${escHtml(iss.title)}</div>\n <div style=\"color:var(--dim);font-size:10px;margin-bottom:1rem\">\n ${iss.id} | ${iss.projectId}\n ${iss.linearIdentifier ? ' | <a href=\"' + iss.linearUrl + '\" style=\"color:var(--cyan)\" target=\"_blank\">' + iss.linearIdentifier + '</a>' : ''}\n </div>\n\n <div class=\"detail-section\">\n <h4>STATUS</h4>\n <select class=\"filter-select\" onchange=\"changeStatus('${iss.id}', this.value)\" style=\"width:100%\">\n ${statusOptions}\n <option value=\"cancelled\"${iss.status==='cancelled'?' selected':''}>CANCELLED</option>\n </select>\n </div>\n\n <div class=\"detail-section\">\n <h4>DESCRIPTION</h4>\n <p style=\"white-space:pre-wrap\">${escHtml(iss.description) || '<span style=\"color:var(--dim)\">no description</span>'}</p>\n </div>\n\n ${iss.relevantFiles.length ? '<div class=\"detail-section\"><h4>RELEVANT FILES</h4><ul>' + iss.relevantFiles.map(f => '<li>' + escHtml(f) + '</li>').join('') + '</ul></div>' : ''}\n\n ${iss.acceptanceCriteria.length ? '<div class=\"detail-section\"><h4>ACCEPTANCE CRITERIA</h4><ul>' + iss.acceptanceCriteria.map(c => '<li>' + escHtml(c) + '</li>').join('') + '</ul></div>' : ''}\n\n ${iss.dependencies.length ? '<div class=\"detail-section\"><h4>DEPENDENCIES</h4><p>' + iss.dependencies.join(', ') + '</p></div>' : ''}\n\n <div class=\"detail-section\">\n <h4>ACTIVITY (${events.length})</h4>\n ${events.map(ev => `\n <div class=\"event-item\">\n <span class=\"ev-type\">${ev.type}</span>\n ${ev.content ? ': ' + escHtml(ev.content).slice(0, 100) : ''}\n ${ev.oldValue && ev.newValue ? ': ' + ev.oldValue + ' \u2192 ' + ev.newValue : ''}\n <span style=\"float:right\">${formatTimeAgo(ev.createdAt)}</span>\n </div>\n `).join('')}\n </div>\n\n <div class=\"detail-section\" style=\"margin-top:1.5rem\">\n <h4>ADD COMMENT</h4>\n <textarea id=\"comment-input\" style=\"width:100%;background:var(--bg3);color:var(--white);border:1px solid var(--border);padding:6px;font-family:inherit;font-size:12px;min-height:60px;border-radius:3px\" placeholder=\"\uCF54\uBA58\uD2B8 \uC785\uB825...\"></textarea>\n <button class=\"btn btn-primary\" style=\"margin-top:4px\" onclick=\"addComment('${iss.id}')\">COMMENT</button>\n </div>\n\n <div class=\"form-actions\" style=\"margin-top:1.5rem;justify-content:flex-start\">\n <button class=\"btn\" style=\"color:var(--red);border-color:var(--red)\" onclick=\"deleteIssue('${iss.id}')\">DELETE</button>\n </div>\n `;\n\n panel.classList.add('active');\n }\n\n function closeDetail() {\n document.getElementById('detail-panel').classList.remove('active');\n }\n\n async function changeStatus(id, status) {\n await gql(`mutation($id:ID!,$s:IssueStatus!){changeIssueStatus(id:$id,status:$s){id}}`, { id, s: status });\n loadIssues();\n }\n\n async function addComment(id) {\n const input = document.getElementById('comment-input');\n const content = input.value.trim();\n if (!content) return;\n await gql(`mutation($id:ID!,$c:String!){addComment(issueId:$id,content:$c){id}}`, { id, c: content });\n input.value = '';\n openDetail(id);\n }\n\n async function deleteIssue(id) {\n if (!confirm('Delete this issue?')) return;\n await gql(`mutation($id:ID!){deleteIssue(id:$id)}`, { id });\n closeDetail();\n loadIssues();\n }\n\n // \uC774\uC288 \uC0DD\uC131\n function openCreateModal() {\n const sel = document.getElementById('new-project');\n sel.innerHTML = [...projects].map(p => '<option value=\"' + p + '\">' + p + '</option>').join('');\n if (sel.options.length === 0) {\n sel.innerHTML = '<option value=\"default\">default</option>';\n }\n document.getElementById('create-modal').classList.add('active');\n document.getElementById('new-title').focus();\n }\n\n function closeCreateModal() {\n document.getElementById('create-modal').classList.remove('active');\n }\n\n async function createIssue() {\n const title = document.getElementById('new-title').value.trim();\n if (!title) { alert('Title required'); return; }\n\n const input = {\n projectId: document.getElementById('new-project').value || 'default',\n title,\n description: document.getElementById('new-desc').value,\n priority: document.getElementById('new-priority').value,\n status: document.getElementById('new-status').value,\n labels: document.getElementById('new-labels').value.split(',').map(s => s.trim()).filter(Boolean),\n relevantFiles: document.getElementById('new-files').value.split(',').map(s => s.trim()).filter(Boolean),\n };\n\n await gql(`\n mutation CreateIssue($input: CreateIssueInput!) {\n createIssue(input: $input) { id }\n }\n `, { input });\n\n closeCreateModal();\n // \uD3FC \uCD08\uAE30\uD654\n document.getElementById('new-title').value = '';\n document.getElementById('new-desc').value = '';\n document.getElementById('new-labels').value = '';\n document.getElementById('new-files').value = '';\n loadIssues();\n }\n\n // \uC720\uD2F8\n function escHtml(s) {\n if (!s) return '';\n return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');\n }\n\n function formatTimeAgo(iso) {\n if (!iso) return '';\n const diff = Date.now() - new Date(iso).getTime();\n const mins = Math.floor(diff / 60000);\n if (mins < 1) return 'just now';\n if (mins < 60) return mins + 'm ago';\n const hrs = Math.floor(mins / 60);\n if (hrs < 24) return hrs + 'h ago';\n const days = Math.floor(hrs / 24);\n return days + 'd ago';\n }\n\n let searchTimer;\n function debounceSearch() {\n clearTimeout(searchTimer);\n searchTimer = setTimeout(loadIssues, 300);\n }\n\n function applyFilter() {\n loadIssues();\n }\n\n // \uD0A4\uBCF4\uB4DC \uB2E8\uCD95\uD0A4\n document.addEventListener('keydown', e => {\n if (e.key === 'Escape') {\n closeCreateModal();\n closeDetail();\n }\n if (e.key === 'n' && !e.target.closest('input,textarea,select')) {\n e.preventDefault();\n openCreateModal();\n }\n });\n\n // \uCD08\uAE30 \uB85C\uB4DC\n loadIssues();\n // 30\uCD08 \uAC04\uACA9 \uC790\uB3D9 \uC0C8\uB85C\uACE0\uCE68\n setInterval(loadIssues, 30000);\n </script>\n</body>\n</html>";
1
+ export declare const ISSUE_BOARD_HTML = "<!DOCTYPE html>\n<html lang=\"ko\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>OpenSwarm :: Issues</title>\n <style>\n :root {\n --bg: #0a0c0a;\n --bg2: #0d100d;\n --bg3: #111411;\n --green: #00ff41;\n --green-dim: #003a00;\n --green-mid: #00aa00;\n --green-lo: #005500;\n --cyan: #00ccdd;\n --cyan-dim: #003344;\n --amber: #ffaa00;\n --red: #ff3333;\n --white: #ccddcc;\n --dim: #445544;\n --border: #1a2a1a;\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: 'Cascadia Code', 'JetBrains Mono', 'Fira Code', monospace;\n background: var(--bg);\n color: var(--white);\n font-size: 13px;\n line-height: 1.4;\n height: 100vh;\n display: flex;\n flex-direction: column;\n }\n\n /* Header */\n header {\n height: 38px;\n background: var(--bg2);\n border-bottom: 1px solid var(--border);\n display: flex;\n align-items: center;\n padding: 0 1rem;\n gap: 0.75rem;\n flex-shrink: 0;\n }\n .hdr-logo { color: var(--green); font-weight: bold; font-size: 14px; letter-spacing: 0.15em; text-decoration: none; }\n .hdr-sep { color: var(--dim); margin: 0 0.5rem; }\n .hdr-sub { color: var(--cyan); font-size: 12px; letter-spacing: 0.1em; }\n .hdr-right { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }\n\n /* Toolbar */\n .toolbar {\n background: var(--bg2);\n border-bottom: 1px solid var(--border);\n padding: 6px 1rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n flex-shrink: 0;\n }\n .btn {\n background: var(--bg3);\n color: var(--green);\n border: 1px solid var(--green-dim);\n padding: 3px 10px;\n font-family: inherit;\n font-size: 11px;\n cursor: pointer;\n border-radius: 3px;\n }\n .btn:hover { background: var(--green-dim); }\n .btn-primary { color: var(--cyan); border-color: var(--cyan-dim); }\n .btn-primary:hover { background: var(--cyan-dim); }\n .filter-select {\n background: var(--bg3);\n color: var(--white);\n border: 1px solid var(--border);\n padding: 3px 6px;\n font-family: inherit;\n font-size: 11px;\n border-radius: 3px;\n }\n .search-input {\n background: var(--bg3);\n color: var(--white);\n border: 1px solid var(--border);\n padding: 3px 8px;\n font-family: inherit;\n font-size: 11px;\n width: 200px;\n border-radius: 3px;\n }\n .search-input::placeholder { color: var(--dim); }\n .stats-bar { margin-left: auto; color: var(--dim); font-size: 11px; }\n .stats-bar span { margin: 0 0.5rem; }\n .stats-val { color: var(--green); }\n\n /* Kanban Board */\n .board {\n flex: 1;\n display: flex;\n gap: 2px;\n padding: 8px;\n overflow-x: auto;\n min-height: 0;\n }\n .column {\n flex: 1;\n min-width: 220px;\n max-width: 320px;\n background: var(--bg2);\n border: 1px solid var(--border);\n border-radius: 4px;\n display: flex;\n flex-direction: column;\n }\n .col-header {\n padding: 6px 10px;\n background: var(--bg3);\n border-bottom: 1px solid var(--border);\n font-size: 11px;\n letter-spacing: 0.1em;\n display: flex;\n align-items: center;\n gap: 6px;\n }\n .col-header .count {\n background: var(--green-dim);\n color: var(--green);\n padding: 1px 6px;\n border-radius: 8px;\n font-size: 10px;\n }\n .col-body {\n flex: 1;\n overflow-y: auto;\n padding: 4px;\n }\n\n /* Issue Card */\n .card {\n background: var(--bg3);\n border: 1px solid var(--border);\n border-radius: 3px;\n padding: 8px;\n margin-bottom: 4px;\n cursor: pointer;\n transition: border-color 0.15s;\n }\n .card:hover { border-color: var(--green-lo); }\n .card-title { font-size: 12px; color: var(--white); margin-bottom: 4px; }\n .card-meta { font-size: 10px; color: var(--dim); display: flex; gap: 6px; flex-wrap: wrap; }\n .card-priority {\n display: inline-block;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n margin-right: 4px;\n }\n .p-urgent { background: var(--red); }\n .p-high { background: var(--amber); }\n .p-medium { background: var(--cyan); }\n .p-low { background: var(--dim); }\n .p-none { background: transparent; border: 1px solid var(--dim); }\n .card-label {\n font-size: 9px;\n padding: 1px 4px;\n border-radius: 2px;\n background: var(--green-dim);\n color: var(--green-mid);\n }\n .card-id { color: var(--dim); font-size: 9px; }\n\n /* Modal */\n .modal-overlay {\n display: none;\n position: fixed;\n inset: 0;\n background: rgba(0,0,0,0.7);\n z-index: 100;\n justify-content: center;\n align-items: center;\n }\n .modal-overlay.active { display: flex; }\n .modal {\n background: var(--bg2);\n border: 1px solid var(--green-dim);\n border-radius: 6px;\n width: 90%;\n max-width: 600px;\n max-height: 85vh;\n overflow-y: auto;\n padding: 1.5rem;\n }\n .modal h3 { color: var(--green); font-size: 14px; margin-bottom: 1rem; letter-spacing: 0.1em; }\n .form-group { margin-bottom: 0.75rem; }\n .form-group label { display: block; color: var(--dim); font-size: 10px; margin-bottom: 3px; letter-spacing: 0.05em; }\n .form-group input, .form-group textarea, .form-group select {\n width: 100%;\n background: var(--bg3);\n color: var(--white);\n border: 1px solid var(--border);\n padding: 6px 8px;\n font-family: inherit;\n font-size: 12px;\n border-radius: 3px;\n }\n .form-group textarea { min-height: 80px; resize: vertical; }\n .form-actions { display: flex; gap: 0.5rem; margin-top: 1rem; justify-content: flex-end; }\n .form-actions .btn { padding: 5px 16px; }\n\n /* Detail panel */\n .detail-panel {\n display: none;\n position: fixed;\n top: 0;\n right: 0;\n width: 420px;\n height: 100%;\n background: var(--bg2);\n border-left: 1px solid var(--green-dim);\n z-index: 90;\n overflow-y: auto;\n padding: 1rem;\n }\n .detail-panel.active { display: block; }\n .detail-close { float: right; cursor: pointer; color: var(--dim); font-size: 16px; }\n .detail-title { color: var(--green); font-size: 14px; margin-bottom: 0.5rem; margin-right: 2rem; }\n .detail-section { margin-top: 1rem; }\n .detail-section h4 { color: var(--cyan); font-size: 11px; letter-spacing: 0.05em; margin-bottom: 0.25rem; }\n .detail-section p, .detail-section ul { color: var(--white); font-size: 12px; }\n .detail-section ul { padding-left: 1rem; }\n .event-item { font-size: 11px; color: var(--dim); padding: 3px 0; border-bottom: 1px solid var(--border); }\n .event-item .ev-type { color: var(--amber); }\n </style>\n</head>\n<body>\n <header>\n <a href=\"/\" class=\"hdr-logo\">OpenSwarm</a>\n <span class=\"hdr-sep\">::</span>\n <span class=\"hdr-sub\">ISSUE TRACKER</span>\n <div class=\"hdr-right\">\n <span id=\"stats-summary\" style=\"color:var(--dim);font-size:11px\"></span>\n </div>\n </header>\n\n <div class=\"toolbar\">\n <button class=\"btn btn-primary\" onclick=\"openCreateModal()\">+ NEW ISSUE</button>\n <select class=\"filter-select\" id=\"filter-project\" onchange=\"applyFilter()\">\n <option value=\"\">all projects</option>\n </select>\n <select class=\"filter-select\" id=\"filter-priority\" onchange=\"applyFilter()\">\n <option value=\"\">all priorities</option>\n <option value=\"urgent\">urgent</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n </select>\n <input type=\"text\" class=\"search-input\" id=\"search-input\" placeholder=\"search issues...\" oninput=\"debounceSearch()\">\n <div class=\"stats-bar\">\n <span>total: <span class=\"stats-val\" id=\"stat-total\">0</span></span>\n <span>open: <span class=\"stats-val\" id=\"stat-open\">0</span></span>\n <span>done: <span class=\"stats-val\" id=\"stat-done\">0</span></span>\n </div>\n </div>\n\n <div class=\"board\" id=\"board\">\n <!-- \uCE78\uBC18 \uCE7C\uB7FC\uC740 JS\uC5D0\uC11C \uB3D9\uC801 \uC0DD\uC131 -->\n </div>\n\n <!-- \uC774\uC288 \uC0DD\uC131 \uBAA8\uB2EC -->\n <div class=\"modal-overlay\" id=\"create-modal\">\n <div class=\"modal\">\n <h3>NEW ISSUE</h3>\n <div class=\"form-group\">\n <label>PROJECT</label>\n <select id=\"new-project\"></select>\n </div>\n <div class=\"form-group\">\n <label>TITLE</label>\n <input type=\"text\" id=\"new-title\" placeholder=\"\uC774\uC288 \uC81C\uBAA9\">\n </div>\n <div class=\"form-group\">\n <label>DESCRIPTION</label>\n <textarea id=\"new-desc\" placeholder=\"\uC0C1\uC138 \uC124\uBA85...\"></textarea>\n </div>\n <div style=\"display:flex;gap:0.75rem\">\n <div class=\"form-group\" style=\"flex:1\">\n <label>PRIORITY</label>\n <select id=\"new-priority\">\n <option value=\"medium\">medium</option>\n <option value=\"urgent\">urgent</option>\n <option value=\"high\">high</option>\n <option value=\"low\">low</option>\n <option value=\"none\">none</option>\n </select>\n </div>\n <div class=\"form-group\" style=\"flex:1\">\n <label>STATUS</label>\n <select id=\"new-status\">\n <option value=\"backlog\">backlog</option>\n <option value=\"todo\">todo</option>\n <option value=\"in_progress\">in_progress</option>\n </select>\n </div>\n </div>\n <div class=\"form-group\">\n <label>LABELS (comma separated)</label>\n <input type=\"text\" id=\"new-labels\" placeholder=\"bug, feature, ...\">\n </div>\n <div class=\"form-group\">\n <label>RELEVANT FILES (comma separated)</label>\n <input type=\"text\" id=\"new-files\" placeholder=\"src/foo.ts, src/bar.ts\">\n </div>\n <div class=\"form-actions\">\n <button class=\"btn\" onclick=\"closeCreateModal()\">CANCEL</button>\n <button class=\"btn btn-primary\" onclick=\"createIssue()\">CREATE</button>\n </div>\n </div>\n </div>\n\n <!-- \uC774\uC288 \uC0C1\uC138 \uD328\uB110 -->\n <div class=\"detail-panel\" id=\"detail-panel\">\n <span class=\"detail-close\" onclick=\"closeDetail()\">&times;</span>\n <div id=\"detail-content\"></div>\n </div>\n\n <script>\n const COLUMNS = [\n { status: 'backlog', label: 'BACKLOG', color: 'var(--dim)' },\n { status: 'todo', label: 'TODO', color: 'var(--white)' },\n { status: 'in_progress', label: 'IN PROGRESS', color: 'var(--amber)' },\n { status: 'in_review', label: 'IN REVIEW', color: 'var(--cyan)' },\n { status: 'done', label: 'DONE', color: 'var(--green)' },\n { status: 'cancelled', label: 'CANCELLED', color: 'var(--red)' },\n ];\n\n let allIssues = [];\n let projects = new Set();\n\n // GraphQL helper\n async function gql(query, variables = {}) {\n const res = await fetch('/graphql', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ query, variables }),\n });\n const json = await res.json();\n if (json.errors) {\n console.error('GraphQL errors:', json.errors);\n throw new Error(json.errors[0].message);\n }\n return json.data;\n }\n\n // \uC774\uC288 \uBAA9\uB85D \uB85C\uB4DC\n async function loadIssues() {\n const projectId = document.getElementById('filter-project').value || undefined;\n const priority = document.getElementById('filter-priority').value || undefined;\n const search = document.getElementById('search-input').value || undefined;\n\n const filter = {};\n if (projectId) filter.projectId = projectId;\n if (priority) filter.priority = [priority];\n if (search) filter.search = search;\n\n const data = await gql(`\n query ListIssues($filter: IssueFilterInput) {\n issues(filter: $filter) {\n issues {\n id projectId title description status priority source\n labels assignee relevantFiles dependencies childIds\n linearIdentifier memoryIds createdAt updatedAt closedAt\n }\n total\n }\n issueStats {\n total\n byStatus { status count }\n }\n }\n `, { filter: Object.keys(filter).length > 0 ? filter : null });\n\n allIssues = data.issues.issues;\n\n // \uD504\uB85C\uC81D\uD2B8 \uBAA9\uB85D \uAC31\uC2E0\n for (const iss of allIssues) projects.add(iss.projectId);\n updateProjectFilter();\n\n // \uD1B5\uACC4 \uAC31\uC2E0\n const stats = data.issueStats;\n document.getElementById('stat-total').textContent = stats.total;\n const openCount = stats.byStatus\n .filter(s => !['done', 'cancelled'].includes(s.status))\n .reduce((a, s) => a + s.count, 0);\n const doneCount = stats.byStatus.find(s => s.status === 'done')?.count || 0;\n document.getElementById('stat-open').textContent = openCount;\n document.getElementById('stat-done').textContent = doneCount;\n\n renderBoard();\n }\n\n function updateProjectFilter() {\n const sel = document.getElementById('filter-project');\n const current = sel.value;\n const opts = ['<option value=\"\">all projects</option>'];\n for (const p of projects) {\n opts.push('<option value=\"' + escAttr(p) + '\"' + (p === current ? ' selected' : '') + '>' + escHtml(p) + '</option>');\n }\n sel.innerHTML = opts.join('');\n }\n\n // \uCE78\uBC18 \uBCF4\uB4DC \uB80C\uB354\uB9C1\n function renderBoard() {\n const board = document.getElementById('board');\n board.innerHTML = '';\n\n for (const col of COLUMNS) {\n const issues = allIssues.filter(i => i.status === col.status);\n const colEl = document.createElement('div');\n colEl.className = 'column';\n colEl.innerHTML = `\n <div class=\"col-header\">\n <span style=\"color:${col.color}\">${col.label}</span>\n <span class=\"count\">${issues.length}</span>\n </div>\n <div class=\"col-body\" data-status=\"${col.status}\"></div>\n `;\n\n const body = colEl.querySelector('.col-body');\n for (const iss of issues) {\n body.appendChild(createCard(iss));\n }\n\n // \uB4DC\uB798\uADF8 \uB4DC\uB86D \uC218\uC2E0\n body.addEventListener('dragover', e => { e.preventDefault(); body.style.background = 'var(--green-dim)'; });\n body.addEventListener('dragleave', () => { body.style.background = ''; });\n body.addEventListener('drop', async e => {\n e.preventDefault();\n body.style.background = '';\n const issueId = e.dataTransfer.getData('text/plain');\n if (issueId && col.status) {\n await gql(`mutation($id:ID!,$s:IssueStatus!){changeIssueStatus(id:$id,status:$s){id}}`,\n { id: issueId, s: col.status });\n loadIssues();\n }\n });\n\n board.appendChild(colEl);\n }\n }\n\n function createCard(iss) {\n const card = document.createElement('div');\n card.className = 'card';\n card.draggable = true;\n card.addEventListener('dragstart', e => { e.dataTransfer.setData('text/plain', iss.id); });\n card.addEventListener('click', () => openDetail(iss.id));\n\n const priorityClass = 'p-' + iss.priority;\n const labels = (iss.labels || []).map(l => '<span class=\"card-label\">' + escHtml(l) + '</span>').join('');\n const timeAgo = formatTimeAgo(iss.updatedAt);\n\n card.innerHTML = `\n <div class=\"card-title\"><span class=\"card-priority ${escAttr(priorityClass)}\"></span>${escHtml(iss.title)}</div>\n <div class=\"card-meta\">\n <span class=\"card-id\">${escHtml(iss.id.slice(0, 6))}</span>\n <span>${escHtml(iss.projectId)}</span>\n ${iss.assignee ? '<span>' + escHtml(iss.assignee) + '</span>' : ''}\n <span>${escHtml(timeAgo)}</span>\n </div>\n ${labels ? '<div class=\"card-meta\" style=\"margin-top:3px\">' + labels + '</div>' : ''}\n `;\n return card;\n }\n\n // \uC774\uC288 \uC0C1\uC138 \uD328\uB110\n async function openDetail(id) {\n const data = await gql(`\n query IssueDetail($id:ID!) {\n issue(id:$id) {\n id projectId title description status priority source\n labels assignee relevantFiles acceptanceCriteria\n dependencies childIds memoryIds createdAt updatedAt closedAt\n linearIdentifier linearUrl\n }\n issueEvents(issueId:$id, limit:20) {\n id type oldValue newValue content actor createdAt\n }\n }\n `, { id });\n\n const iss = data.issue;\n if (!iss) return;\n const events = data.issueEvents;\n\n const panel = document.getElementById('detail-panel');\n const content = document.getElementById('detail-content');\n\n const statusOptions = COLUMNS.map(c =>\n '<option value=\"' + escAttr(c.status) + '\"' + (c.status === iss.status ? ' selected' : '') + '>' + escHtml(c.label) + '</option>'\n ).join('');\n const linearLink = iss.linearIdentifier && iss.linearUrl\n ? ' | <a href=\"' + escAttr(safeUrl(iss.linearUrl)) + '\" style=\"color:var(--cyan)\" target=\"_blank\" rel=\"noopener noreferrer\">' + escHtml(iss.linearIdentifier) + '</a>'\n : '';\n\n content.innerHTML = `\n <div class=\"detail-title\">${escHtml(iss.title)}</div>\n <div style=\"color:var(--dim);font-size:10px;margin-bottom:1rem\">\n ${escHtml(iss.id)} | ${escHtml(iss.projectId)}\n ${linearLink}\n </div>\n\n <div class=\"detail-section\">\n <h4>STATUS</h4>\n <select class=\"filter-select\" onchange=\"changeStatus('${escJsArg(iss.id)}', this.value)\" style=\"width:100%\">\n ${statusOptions}\n </select>\n </div>\n\n <div class=\"detail-section\">\n <h4>DESCRIPTION</h4>\n <p style=\"white-space:pre-wrap\">${escHtml(iss.description) || '<span style=\"color:var(--dim)\">no description</span>'}</p>\n </div>\n\n ${iss.relevantFiles.length ? '<div class=\"detail-section\"><h4>RELEVANT FILES</h4><ul>' + iss.relevantFiles.map(f => '<li>' + escHtml(f) + '</li>').join('') + '</ul></div>' : ''}\n\n ${iss.acceptanceCriteria.length ? '<div class=\"detail-section\"><h4>ACCEPTANCE CRITERIA</h4><ul>' + iss.acceptanceCriteria.map(c => '<li>' + escHtml(c) + '</li>').join('') + '</ul></div>' : ''}\n\n ${iss.dependencies.length ? '<div class=\"detail-section\"><h4>DEPENDENCIES</h4><p>' + iss.dependencies.map(d => escHtml(d)).join(', ') + '</p></div>' : ''}\n\n <div class=\"detail-section\">\n <h4>ACTIVITY (${events.length})</h4>\n ${events.map(ev => `\n <div class=\"event-item\">\n <span class=\"ev-type\">${escHtml(ev.type)}</span>\n ${ev.content ? ': ' + escHtml(ev.content).slice(0, 100) : ''}\n ${ev.oldValue && ev.newValue ? ': ' + escHtml(ev.oldValue) + ' \u2192 ' + escHtml(ev.newValue) : ''}\n <span style=\"float:right\">${escHtml(formatTimeAgo(ev.createdAt))}</span>\n </div>\n `).join('')}\n </div>\n\n <div class=\"detail-section\" style=\"margin-top:1.5rem\">\n <h4>ADD COMMENT</h4>\n <textarea id=\"comment-input\" style=\"width:100%;background:var(--bg3);color:var(--white);border:1px solid var(--border);padding:6px;font-family:inherit;font-size:12px;min-height:60px;border-radius:3px\" placeholder=\"\uCF54\uBA58\uD2B8 \uC785\uB825...\"></textarea>\n <button class=\"btn btn-primary\" style=\"margin-top:4px\" onclick=\"addComment('${escJsArg(iss.id)}')\">COMMENT</button>\n </div>\n\n <div class=\"form-actions\" style=\"margin-top:1.5rem;justify-content:flex-start\">\n <button class=\"btn\" style=\"color:var(--red);border-color:var(--red)\" onclick=\"deleteIssue('${escJsArg(iss.id)}')\">DELETE</button>\n </div>\n `;\n\n panel.classList.add('active');\n }\n\n function closeDetail() {\n document.getElementById('detail-panel').classList.remove('active');\n }\n\n async function changeStatus(id, status) {\n await gql(`mutation($id:ID!,$s:IssueStatus!){changeIssueStatus(id:$id,status:$s){id}}`, { id, s: status });\n loadIssues();\n }\n\n async function addComment(id) {\n const input = document.getElementById('comment-input');\n const content = input.value.trim();\n if (!content) return;\n await gql(`mutation($id:ID!,$c:String!){addComment(issueId:$id,content:$c){id}}`, { id, c: content });\n input.value = '';\n openDetail(id);\n }\n\n async function deleteIssue(id) {\n if (!confirm('Delete this issue?')) return;\n await gql(`mutation($id:ID!){deleteIssue(id:$id)}`, { id });\n closeDetail();\n loadIssues();\n }\n\n // \uC774\uC288 \uC0DD\uC131\n function openCreateModal() {\n const sel = document.getElementById('new-project');\n sel.innerHTML = [...projects].map(p => '<option value=\"' + escAttr(p) + '\">' + escHtml(p) + '</option>').join('');\n if (sel.options.length === 0) {\n sel.innerHTML = '<option value=\"default\">default</option>';\n }\n document.getElementById('create-modal').classList.add('active');\n document.getElementById('new-title').focus();\n }\n\n function closeCreateModal() {\n document.getElementById('create-modal').classList.remove('active');\n }\n\n async function createIssue() {\n const title = document.getElementById('new-title').value.trim();\n if (!title) { alert('Title required'); return; }\n\n const input = {\n projectId: document.getElementById('new-project').value || 'default',\n title,\n description: document.getElementById('new-desc').value,\n priority: document.getElementById('new-priority').value,\n status: document.getElementById('new-status').value,\n labels: document.getElementById('new-labels').value.split(',').map(s => s.trim()).filter(Boolean),\n relevantFiles: document.getElementById('new-files').value.split(',').map(s => s.trim()).filter(Boolean),\n };\n\n await gql(`\n mutation CreateIssue($input: CreateIssueInput!) {\n createIssue(input: $input) { id }\n }\n `, { input });\n\n closeCreateModal();\n // \uD3FC \uCD08\uAE30\uD654\n document.getElementById('new-title').value = '';\n document.getElementById('new-desc').value = '';\n document.getElementById('new-labels').value = '';\n document.getElementById('new-files').value = '';\n loadIssues();\n }\n\n // \uC720\uD2F8\n function escHtml(s) {\n if (!s) return '';\n return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;').replace(/'/g,'&#39;');\n }\n\n function escAttr(s) {\n return escHtml(s);\n }\n\n function escJsArg(s) {\n return escAttr(String(s).replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\"));\n }\n\n function safeUrl(url) {\n try {\n const parsed = new URL(url, window.location.origin);\n return ['http:', 'https:'].includes(parsed.protocol) ? parsed.href : '#';\n } catch {\n return '#';\n }\n }\n\n function formatTimeAgo(iso) {\n if (!iso) return '';\n const diff = Date.now() - new Date(iso).getTime();\n const mins = Math.floor(diff / 60000);\n if (mins < 1) return 'just now';\n if (mins < 60) return mins + 'm ago';\n const hrs = Math.floor(mins / 60);\n if (hrs < 24) return hrs + 'h ago';\n const days = Math.floor(hrs / 24);\n return days + 'd ago';\n }\n\n let searchTimer;\n function debounceSearch() {\n clearTimeout(searchTimer);\n searchTimer = setTimeout(loadIssues, 300);\n }\n\n function applyFilter() {\n loadIssues();\n }\n\n // \uD0A4\uBCF4\uB4DC \uB2E8\uCD95\uD0A4\n document.addEventListener('keydown', e => {\n if (e.key === 'Escape') {\n closeCreateModal();\n closeDetail();\n }\n if (e.key === 'n' && !e.target.closest('input,textarea,select')) {\n e.preventDefault();\n openCreateModal();\n }\n });\n\n // \uCD08\uAE30 \uB85C\uB4DC\n loadIssues();\n // 30\uCD08 \uAC04\uACA9 \uC790\uB3D9 \uC0C8\uB85C\uACE0\uCE68\n setInterval(loadIssues, 30000);\n </script>\n</body>\n</html>";
2
2
  //# sourceMappingURL=issueBoardHtml.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"issueBoardHtml.d.ts","sourceRoot":"","sources":["../../src/issues/issueBoardHtml.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,gBAAgB,+nwBA8pBrB,CAAC"}
1
+ {"version":3,"file":"issueBoardHtml.d.ts","sourceRoot":"","sources":["../../src/issues/issueBoardHtml.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,gBAAgB,m4xBAkrBrB,CAAC"}
@@ -338,6 +338,7 @@ export const ISSUE_BOARD_HTML = `<!DOCTYPE html>
338
338
  { status: 'in_progress', label: 'IN PROGRESS', color: 'var(--amber)' },
339
339
  { status: 'in_review', label: 'IN REVIEW', color: 'var(--cyan)' },
340
340
  { status: 'done', label: 'DONE', color: 'var(--green)' },
341
+ { status: 'cancelled', label: 'CANCELLED', color: 'var(--red)' },
341
342
  ];
342
343
 
343
344
  let allIssues = [];
@@ -410,7 +411,7 @@ export const ISSUE_BOARD_HTML = `<!DOCTYPE html>
410
411
  const current = sel.value;
411
412
  const opts = ['<option value="">all projects</option>'];
412
413
  for (const p of projects) {
413
- opts.push('<option value="' + p + '"' + (p === current ? ' selected' : '') + '>' + p + '</option>');
414
+ opts.push('<option value="' + escAttr(p) + '"' + (p === current ? ' selected' : '') + '>' + escHtml(p) + '</option>');
414
415
  }
415
416
  sel.innerHTML = opts.join('');
416
417
  }
@@ -463,16 +464,16 @@ export const ISSUE_BOARD_HTML = `<!DOCTYPE html>
463
464
  card.addEventListener('click', () => openDetail(iss.id));
464
465
 
465
466
  const priorityClass = 'p-' + iss.priority;
466
- const labels = (iss.labels || []).map(l => '<span class="card-label">' + l + '</span>').join('');
467
+ const labels = (iss.labels || []).map(l => '<span class="card-label">' + escHtml(l) + '</span>').join('');
467
468
  const timeAgo = formatTimeAgo(iss.updatedAt);
468
469
 
469
470
  card.innerHTML = \`
470
- <div class="card-title"><span class="card-priority \${priorityClass}"></span>\${escHtml(iss.title)}</div>
471
+ <div class="card-title"><span class="card-priority \${escAttr(priorityClass)}"></span>\${escHtml(iss.title)}</div>
471
472
  <div class="card-meta">
472
- <span class="card-id">\${iss.id.slice(0, 6)}</span>
473
- <span>\${iss.projectId}</span>
474
- \${iss.assignee ? '<span>' + iss.assignee + '</span>' : ''}
475
- <span>\${timeAgo}</span>
473
+ <span class="card-id">\${escHtml(iss.id.slice(0, 6))}</span>
474
+ <span>\${escHtml(iss.projectId)}</span>
475
+ \${iss.assignee ? '<span>' + escHtml(iss.assignee) + '</span>' : ''}
476
+ <span>\${escHtml(timeAgo)}</span>
476
477
  </div>
477
478
  \${labels ? '<div class="card-meta" style="margin-top:3px">' + labels + '</div>' : ''}
478
479
  \`;
@@ -503,21 +504,23 @@ export const ISSUE_BOARD_HTML = `<!DOCTYPE html>
503
504
  const content = document.getElementById('detail-content');
504
505
 
505
506
  const statusOptions = COLUMNS.map(c =>
506
- '<option value="' + c.status + '"' + (c.status === iss.status ? ' selected' : '') + '>' + c.label + '</option>'
507
+ '<option value="' + escAttr(c.status) + '"' + (c.status === iss.status ? ' selected' : '') + '>' + escHtml(c.label) + '</option>'
507
508
  ).join('');
509
+ const linearLink = iss.linearIdentifier && iss.linearUrl
510
+ ? ' | <a href="' + escAttr(safeUrl(iss.linearUrl)) + '" style="color:var(--cyan)" target="_blank" rel="noopener noreferrer">' + escHtml(iss.linearIdentifier) + '</a>'
511
+ : '';
508
512
 
509
513
  content.innerHTML = \`
510
514
  <div class="detail-title">\${escHtml(iss.title)}</div>
511
515
  <div style="color:var(--dim);font-size:10px;margin-bottom:1rem">
512
- \${iss.id} | \${iss.projectId}
513
- \${iss.linearIdentifier ? ' | <a href="' + iss.linearUrl + '" style="color:var(--cyan)" target="_blank">' + iss.linearIdentifier + '</a>' : ''}
516
+ \${escHtml(iss.id)} | \${escHtml(iss.projectId)}
517
+ \${linearLink}
514
518
  </div>
515
519
 
516
520
  <div class="detail-section">
517
521
  <h4>STATUS</h4>
518
- <select class="filter-select" onchange="changeStatus('\${iss.id}', this.value)" style="width:100%">
522
+ <select class="filter-select" onchange="changeStatus('\${escJsArg(iss.id)}', this.value)" style="width:100%">
519
523
  \${statusOptions}
520
- <option value="cancelled"\${iss.status==='cancelled'?' selected':''}>CANCELLED</option>
521
524
  </select>
522
525
  </div>
523
526
 
@@ -530,16 +533,16 @@ export const ISSUE_BOARD_HTML = `<!DOCTYPE html>
530
533
 
531
534
  \${iss.acceptanceCriteria.length ? '<div class="detail-section"><h4>ACCEPTANCE CRITERIA</h4><ul>' + iss.acceptanceCriteria.map(c => '<li>' + escHtml(c) + '</li>').join('') + '</ul></div>' : ''}
532
535
 
533
- \${iss.dependencies.length ? '<div class="detail-section"><h4>DEPENDENCIES</h4><p>' + iss.dependencies.join(', ') + '</p></div>' : ''}
536
+ \${iss.dependencies.length ? '<div class="detail-section"><h4>DEPENDENCIES</h4><p>' + iss.dependencies.map(d => escHtml(d)).join(', ') + '</p></div>' : ''}
534
537
 
535
538
  <div class="detail-section">
536
539
  <h4>ACTIVITY (\${events.length})</h4>
537
540
  \${events.map(ev => \`
538
541
  <div class="event-item">
539
- <span class="ev-type">\${ev.type}</span>
542
+ <span class="ev-type">\${escHtml(ev.type)}</span>
540
543
  \${ev.content ? ': ' + escHtml(ev.content).slice(0, 100) : ''}
541
- \${ev.oldValue && ev.newValue ? ': ' + ev.oldValue + ' → ' + ev.newValue : ''}
542
- <span style="float:right">\${formatTimeAgo(ev.createdAt)}</span>
544
+ \${ev.oldValue && ev.newValue ? ': ' + escHtml(ev.oldValue) + ' → ' + escHtml(ev.newValue) : ''}
545
+ <span style="float:right">\${escHtml(formatTimeAgo(ev.createdAt))}</span>
543
546
  </div>
544
547
  \`).join('')}
545
548
  </div>
@@ -547,11 +550,11 @@ export const ISSUE_BOARD_HTML = `<!DOCTYPE html>
547
550
  <div class="detail-section" style="margin-top:1.5rem">
548
551
  <h4>ADD COMMENT</h4>
549
552
  <textarea id="comment-input" style="width:100%;background:var(--bg3);color:var(--white);border:1px solid var(--border);padding:6px;font-family:inherit;font-size:12px;min-height:60px;border-radius:3px" placeholder="코멘트 입력..."></textarea>
550
- <button class="btn btn-primary" style="margin-top:4px" onclick="addComment('\${iss.id}')">COMMENT</button>
553
+ <button class="btn btn-primary" style="margin-top:4px" onclick="addComment('\${escJsArg(iss.id)}')">COMMENT</button>
551
554
  </div>
552
555
 
553
556
  <div class="form-actions" style="margin-top:1.5rem;justify-content:flex-start">
554
- <button class="btn" style="color:var(--red);border-color:var(--red)" onclick="deleteIssue('\${iss.id}')">DELETE</button>
557
+ <button class="btn" style="color:var(--red);border-color:var(--red)" onclick="deleteIssue('\${escJsArg(iss.id)}')">DELETE</button>
555
558
  </div>
556
559
  \`;
557
560
 
@@ -586,7 +589,7 @@ export const ISSUE_BOARD_HTML = `<!DOCTYPE html>
586
589
  // 이슈 생성
587
590
  function openCreateModal() {
588
591
  const sel = document.getElementById('new-project');
589
- sel.innerHTML = [...projects].map(p => '<option value="' + p + '">' + p + '</option>').join('');
592
+ sel.innerHTML = [...projects].map(p => '<option value="' + escAttr(p) + '">' + escHtml(p) + '</option>').join('');
590
593
  if (sel.options.length === 0) {
591
594
  sel.innerHTML = '<option value="default">default</option>';
592
595
  }
@@ -630,7 +633,24 @@ export const ISSUE_BOARD_HTML = `<!DOCTYPE html>
630
633
  // 유틸
631
634
  function escHtml(s) {
632
635
  if (!s) return '';
633
- return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
636
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
637
+ }
638
+
639
+ function escAttr(s) {
640
+ return escHtml(s);
641
+ }
642
+
643
+ function escJsArg(s) {
644
+ return escAttr(String(s).replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'"));
645
+ }
646
+
647
+ function safeUrl(url) {
648
+ try {
649
+ const parsed = new URL(url, window.location.origin);
650
+ return ['http:', 'https:'].includes(parsed.protocol) ? parsed.href : '#';
651
+ } catch {
652
+ return '#';
653
+ }
634
654
  }
635
655
 
636
656
  function formatTimeAgo(iso) {
@@ -1 +1 @@
1
- {"version":3,"file":"issueBoardHtml.js","sourceRoot":"","sources":["../../src/issues/issueBoardHtml.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,wCAAwC;AACxC,sBAAsB;AACtB,iCAAiC;AACjC,+CAA+C;AAE/C,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA8pBxB,CAAC"}
1
+ {"version":3,"file":"issueBoardHtml.js","sourceRoot":"","sources":["../../src/issues/issueBoardHtml.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,wCAAwC;AACxC,sBAAsB;AACtB,iCAAiC;AACjC,+CAA+C;AAE/C,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAkrBxB,CAAC"}
@@ -4,7 +4,7 @@ import type { IssueStatus } from './schema.js';
4
4
  * Linear 브릿지 초기화
5
5
  * config.yaml에서 linear.enabled: true 일 때만 호출
6
6
  */
7
- export declare function initLinearBridge(apiKey: string, teamId: string): void;
7
+ export declare function initLinearBridge(apiKey: string, teamId: string): Promise<void>;
8
8
  /**
9
9
  * Linear → 로컬: Linear 이슈를 로컬 DB에 동기화
10
10
  */
@@ -1 +1 @@
1
- {"version":3,"file":"linearBridge.d.ts","sourceRoot":"","sources":["../../src/issues/linearBridge.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EAAS,WAAW,EAAiB,MAAM,aAAa,CAAC;AAMrE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CASrE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,gBAAgB,EACvB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9C,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAiD/C;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA0CxB;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,WAAW,GACrB,OAAO,CAAC,OAAO,CAAC,CAelB;AA2FD,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C"}
1
+ {"version":3,"file":"linearBridge.d.ts","sourceRoot":"","sources":["../../src/issues/linearBridge.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EAAS,WAAW,EAAiB,MAAM,aAAa,CAAC;AAOrE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAY9E;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,gBAAgB,EACvB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9C,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAkD/C;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2CxB;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,WAAW,GACrB,OAAO,CAAC,OAAO,CAAC,CAgBlB;AA8FD,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C"}