@jcjeon/integration-cli 0.2.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 (264) hide show
  1. package/.gitignore +23 -0
  2. package/.npmignore +21 -0
  3. package/.prettierignore +6 -0
  4. package/.prettierrc +26 -0
  5. package/AGENTS.md +10 -0
  6. package/CLAUDE.md +10 -0
  7. package/README.md +384 -0
  8. package/apps/server/README.md +294 -0
  9. package/apps/server/eslint.config.mjs +20 -0
  10. package/apps/server/nest-cli.json +8 -0
  11. package/apps/server/package.json +89 -0
  12. package/apps/server/scripts/postinstall.js +53 -0
  13. package/apps/server/src/__mocks__/glob.js +6 -0
  14. package/apps/server/src/__mocks__/uuid.js +5 -0
  15. package/apps/server/src/app.controller.spec.ts +24 -0
  16. package/apps/server/src/app.controller.ts +13 -0
  17. package/apps/server/src/app.module.ts +18 -0
  18. package/apps/server/src/app.service.ts +8 -0
  19. package/apps/server/src/common/ji-paths.ts +41 -0
  20. package/apps/server/src/database/database.module.ts +27 -0
  21. package/apps/server/src/database/entities/agent-changelog.entity.ts +39 -0
  22. package/apps/server/src/database/entities/agent-session.entity.ts +29 -0
  23. package/apps/server/src/database/entities/conversation.entity.ts +41 -0
  24. package/apps/server/src/database/entities/session.entity.ts +16 -0
  25. package/apps/server/src/database/entities/task-agent-run.entity.ts +40 -0
  26. package/apps/server/src/database/entities/task-agent.entity.ts +42 -0
  27. package/apps/server/src/database/entities/task-requirement.entity.ts +27 -0
  28. package/apps/server/src/database/entities/task-run.entity.ts +41 -0
  29. package/apps/server/src/database/entities/task.entity.ts +44 -0
  30. package/apps/server/src/main.ts +65 -0
  31. package/apps/server/src/modules/agents/agent-model-settings.spec.ts +80 -0
  32. package/apps/server/src/modules/agents/agents.module.ts +11 -0
  33. package/apps/server/src/modules/agents/claude/claude-auth.manager.ts +83 -0
  34. package/apps/server/src/modules/agents/claude/claude-pty.manager.ts +380 -0
  35. package/apps/server/src/modules/agents/claude/claude.controller.ts +85 -0
  36. package/apps/server/src/modules/agents/claude/claude.gateway.ts +158 -0
  37. package/apps/server/src/modules/agents/claude/claude.module.ts +18 -0
  38. package/apps/server/src/modules/agents/claude/claude.service.ts +67 -0
  39. package/apps/server/src/modules/agents/claude/dto/create-session.dto.ts +24 -0
  40. package/apps/server/src/modules/agents/claude/dto/resize-session.dto.ts +13 -0
  41. package/apps/server/src/modules/agents/claude/dto/send-input.dto.ts +9 -0
  42. package/apps/server/src/modules/agents/claude/interfaces/claude-session.interface.ts +26 -0
  43. package/apps/server/src/modules/agents/claude/interfaces/pty-event.interface.ts +10 -0
  44. package/apps/server/src/modules/agents/claude/interfaces/stream-event.interface.ts +61 -0
  45. package/apps/server/src/modules/agents/codex/codex-auth.manager.ts +107 -0
  46. package/apps/server/src/modules/agents/codex/codex-session.manager.ts +357 -0
  47. package/apps/server/src/modules/agents/codex/codex.controller.ts +64 -0
  48. package/apps/server/src/modules/agents/codex/codex.gateway.ts +97 -0
  49. package/apps/server/src/modules/agents/codex/codex.module.ts +17 -0
  50. package/apps/server/src/modules/agents/codex/dto/configure-auth.dto.ts +7 -0
  51. package/apps/server/src/modules/agents/gemini/dto/configure-auth.dto.ts +15 -0
  52. package/apps/server/src/modules/agents/gemini/dto/create-session.dto.ts +9 -0
  53. package/apps/server/src/modules/agents/gemini/dto/send-input.dto.ts +9 -0
  54. package/apps/server/src/modules/agents/gemini/gemini-auth.manager.ts +157 -0
  55. package/apps/server/src/modules/agents/gemini/gemini-session.manager.ts +287 -0
  56. package/apps/server/src/modules/agents/gemini/gemini.controller.ts +93 -0
  57. package/apps/server/src/modules/agents/gemini/gemini.gateway.ts +149 -0
  58. package/apps/server/src/modules/agents/gemini/gemini.module.ts +17 -0
  59. package/apps/server/src/modules/agents/gemini/interfaces/gemini-session.interface.ts +18 -0
  60. package/apps/server/src/modules/agents/gemini/interfaces/stream-event.interface.ts +14 -0
  61. package/apps/server/src/modules/agents/session-termination.spec.ts +103 -0
  62. package/apps/server/src/modules/changelog/changelog.controller.ts +20 -0
  63. package/apps/server/src/modules/changelog/changelog.module.ts +14 -0
  64. package/apps/server/src/modules/changelog/changelog.service.spec.ts +531 -0
  65. package/apps/server/src/modules/changelog/changelog.service.ts +690 -0
  66. package/apps/server/src/modules/conversations/conversation.controller.spec.ts +106 -0
  67. package/apps/server/src/modules/conversations/conversation.controller.ts +60 -0
  68. package/apps/server/src/modules/conversations/conversation.module.ts +14 -0
  69. package/apps/server/src/modules/conversations/conversation.service.spec.ts +176 -0
  70. package/apps/server/src/modules/conversations/conversation.service.ts +54 -0
  71. package/apps/server/src/modules/conversations/dto/create-conversation.dto.ts +37 -0
  72. package/apps/server/src/modules/conversations/enums/conversation.enum.ts +13 -0
  73. package/apps/server/src/modules/fs/fs.controller.ts +29 -0
  74. package/apps/server/src/modules/fs/fs.module.ts +8 -0
  75. package/apps/server/src/modules/harness/dto/save-harness.dto.ts +9 -0
  76. package/apps/server/src/modules/harness/harness.controller.spec.ts +95 -0
  77. package/apps/server/src/modules/harness/harness.controller.ts +35 -0
  78. package/apps/server/src/modules/harness/harness.module.ts +11 -0
  79. package/apps/server/src/modules/harness/harness.service.spec.ts +217 -0
  80. package/apps/server/src/modules/harness/harness.service.ts +112 -0
  81. package/apps/server/src/modules/sessions/session.controller.spec.ts +68 -0
  82. package/apps/server/src/modules/sessions/session.controller.ts +43 -0
  83. package/apps/server/src/modules/sessions/session.module.ts +14 -0
  84. package/apps/server/src/modules/sessions/session.service.spec.ts +106 -0
  85. package/apps/server/src/modules/sessions/session.service.ts +35 -0
  86. package/apps/server/src/modules/tasks/dto/create-task.dto.ts +54 -0
  87. package/apps/server/src/modules/tasks/dto/execute-task.dto.ts +22 -0
  88. package/apps/server/src/modules/tasks/dto/merge-file.dto.ts +7 -0
  89. package/apps/server/src/modules/tasks/dto/rerun-task.dto.ts +14 -0
  90. package/apps/server/src/modules/tasks/dto/update-task.dto.ts +55 -0
  91. package/apps/server/src/modules/tasks/task-execution.service.ts +978 -0
  92. package/apps/server/src/modules/tasks/task.gateway.ts +140 -0
  93. package/apps/server/src/modules/tasks/tasks.controller.spec.ts +210 -0
  94. package/apps/server/src/modules/tasks/tasks.controller.ts +139 -0
  95. package/apps/server/src/modules/tasks/tasks.module.ts +30 -0
  96. package/apps/server/src/modules/tasks/tasks.service.spec.ts +552 -0
  97. package/apps/server/src/modules/tasks/tasks.service.ts +333 -0
  98. package/apps/server/test/app.e2e-spec.ts +28 -0
  99. package/apps/server/test/jest-e2e.json +9 -0
  100. package/apps/server/tsconfig.build.json +4 -0
  101. package/apps/server/tsconfig.json +13 -0
  102. package/apps/web/AGENTS.md +7 -0
  103. package/apps/web/CLAUDE.md +1 -0
  104. package/apps/web/README.md +36 -0
  105. package/apps/web/eslint.config.mjs +21 -0
  106. package/apps/web/next-env.d.ts +6 -0
  107. package/apps/web/next.config.ts +7 -0
  108. package/apps/web/package.json +49 -0
  109. package/apps/web/postcss.config.mjs +7 -0
  110. package/apps/web/public/file.svg +1 -0
  111. package/apps/web/public/globe.svg +1 -0
  112. package/apps/web/public/next.svg +1 -0
  113. package/apps/web/public/vercel.svg +1 -0
  114. package/apps/web/public/window.svg +1 -0
  115. package/apps/web/src/app/claude/page.tsx +5 -0
  116. package/apps/web/src/app/codex/page.tsx +126 -0
  117. package/apps/web/src/app/favicon.ico +0 -0
  118. package/apps/web/src/app/gemini/page.tsx +130 -0
  119. package/apps/web/src/app/globals.css +149 -0
  120. package/apps/web/src/app/layout.tsx +40 -0
  121. package/apps/web/src/app/login/page.tsx +67 -0
  122. package/apps/web/src/app/page.tsx +497 -0
  123. package/apps/web/src/app/task/[id]/page.tsx +11 -0
  124. package/apps/web/src/app/test/page.tsx +298 -0
  125. package/apps/web/src/components/ui/Modal.tsx +78 -0
  126. package/apps/web/src/components/ui/WorkingDirPicker.tsx +195 -0
  127. package/apps/web/src/components/ui/__tests__/Modal.test.tsx +68 -0
  128. package/apps/web/src/features/auth/api/__tests__/auth.api.test.ts +83 -0
  129. package/apps/web/src/features/auth/api/auth.api.ts +81 -0
  130. package/apps/web/src/features/auth/hooks/__tests__/useClaudeAuth.test.ts +166 -0
  131. package/apps/web/src/features/auth/hooks/__tests__/useCodexAuth.test.ts +127 -0
  132. package/apps/web/src/features/auth/hooks/__tests__/useGeminiAuth.test.ts +120 -0
  133. package/apps/web/src/features/auth/hooks/useClaudeAuth.ts +88 -0
  134. package/apps/web/src/features/auth/hooks/useCodexAuth.ts +149 -0
  135. package/apps/web/src/features/auth/hooks/useGeminiAuth.ts +125 -0
  136. package/apps/web/src/features/auth/ui/CodexLoginPanel.tsx +302 -0
  137. package/apps/web/src/features/auth/ui/GeminiLoginPanel.tsx +316 -0
  138. package/apps/web/src/features/auth/ui/LoginForm.tsx +190 -0
  139. package/apps/web/src/features/auth/ui/LoginPanel.tsx +114 -0
  140. package/apps/web/src/features/auth/ui/__tests__/LoginPanel.test.tsx +105 -0
  141. package/apps/web/src/features/chat/api/__tests__/sessions.api.test.ts +187 -0
  142. package/apps/web/src/features/chat/api/sessions.api.ts +161 -0
  143. package/apps/web/src/features/chat/container/ClaudePageContainer.tsx +152 -0
  144. package/apps/web/src/features/chat/hooks/__tests__/useCodexSessions.test.ts +131 -0
  145. package/apps/web/src/features/chat/hooks/__tests__/useGeminiSessions.test.ts +130 -0
  146. package/apps/web/src/features/chat/hooks/useAgentModelSettings.ts +54 -0
  147. package/apps/web/src/features/chat/hooks/useClaudeSessions.ts +323 -0
  148. package/apps/web/src/features/chat/hooks/useCodexSessions.ts +275 -0
  149. package/apps/web/src/features/chat/hooks/useGeminiSessions.ts +255 -0
  150. package/apps/web/src/features/chat/hooks/useSessionCommand.ts +66 -0
  151. package/apps/web/src/features/chat/hooks/useSessionRename.ts +61 -0
  152. package/apps/web/src/features/chat/hooks/useSessionWorkingDirectories.ts +34 -0
  153. package/apps/web/src/features/chat/hooks/useUnifiedSessions.ts +156 -0
  154. package/apps/web/src/features/chat/lib/agentModelOptions.ts +72 -0
  155. package/apps/web/src/features/chat/ui/AgentModelPicker.tsx +134 -0
  156. package/apps/web/src/features/chat/ui/AgentSelectModal.tsx +236 -0
  157. package/apps/web/src/features/chat/ui/ChatInput.tsx +162 -0
  158. package/apps/web/src/features/chat/ui/ChatMessage.tsx +204 -0
  159. package/apps/web/src/features/chat/ui/ChatWorkspace.tsx +207 -0
  160. package/apps/web/src/features/chat/ui/CheckingSkeleton.tsx +44 -0
  161. package/apps/web/src/features/chat/ui/ClaudeLoginView.tsx +44 -0
  162. package/apps/web/src/features/chat/ui/PermissionCard.tsx +37 -0
  163. package/apps/web/src/features/chat/ui/SessionSidebar.tsx +280 -0
  164. package/apps/web/src/features/chat/ui/__tests__/AgentSelectModal.test.tsx +58 -0
  165. package/apps/web/src/features/chat/ui/__tests__/ChatInput.test.tsx +134 -0
  166. package/apps/web/src/features/chat/ui/__tests__/ChatMessage.test.tsx +106 -0
  167. package/apps/web/src/features/chat/ui/__tests__/ChatWorkspace.test.tsx +66 -0
  168. package/apps/web/src/features/diff/ui/DiffFileRow.tsx +73 -0
  169. package/apps/web/src/features/diff/ui/DiffHunk.tsx +61 -0
  170. package/apps/web/src/features/diff/ui/FileChangeBadge.tsx +23 -0
  171. package/apps/web/src/features/diff/ui/__tests__/DiffFileRow.test.tsx +40 -0
  172. package/apps/web/src/features/diff/ui/__tests__/DiffHunk.test.tsx +24 -0
  173. package/apps/web/src/features/diff/ui/__tests__/FileChangeBadge.test.tsx +16 -0
  174. package/apps/web/src/features/fs/api/fs.api.ts +14 -0
  175. package/apps/web/src/features/fs/hooks/useDirBrowser.ts +50 -0
  176. package/apps/web/src/features/harness/api/__tests__/harness.api.test.ts +73 -0
  177. package/apps/web/src/features/harness/api/harness.api.ts +46 -0
  178. package/apps/web/src/features/harness/hooks/__tests__/useHarness.test.ts +65 -0
  179. package/apps/web/src/features/harness/hooks/useHarness.ts +66 -0
  180. package/apps/web/src/features/harness/ui/HarnessModal.tsx +171 -0
  181. package/apps/web/src/features/harness/ui/__tests__/HarnessModal.test.tsx +46 -0
  182. package/apps/web/src/features/status/ui/AgentStatusModal.tsx +267 -0
  183. package/apps/web/src/features/status/ui/__tests__/AgentStatusModal.test.tsx +71 -0
  184. package/apps/web/src/features/tasks/api/__tests__/changelog.api.test.ts +89 -0
  185. package/apps/web/src/features/tasks/api/__tests__/tasks.api.test.ts +282 -0
  186. package/apps/web/src/features/tasks/api/changelog.api.ts +52 -0
  187. package/apps/web/src/features/tasks/api/tasks.api.ts +175 -0
  188. package/apps/web/src/features/tasks/container/TaskDetailPageContainer.tsx +69 -0
  189. package/apps/web/src/features/tasks/hooks/__tests__/useChangelogCodeCopy.test.ts +48 -0
  190. package/apps/web/src/features/tasks/hooks/__tests__/useTaskChangelog.test.ts +48 -0
  191. package/apps/web/src/features/tasks/hooks/__tests__/useTaskCreate.test.ts +217 -0
  192. package/apps/web/src/features/tasks/hooks/__tests__/useTaskEdit.test.ts +152 -0
  193. package/apps/web/src/features/tasks/hooks/__tests__/useTaskExecution.test.ts +143 -0
  194. package/apps/web/src/features/tasks/hooks/__tests__/useTaskList.test.ts +168 -0
  195. package/apps/web/src/features/tasks/hooks/__tests__/useTaskNotification.test.ts +125 -0
  196. package/apps/web/src/features/tasks/hooks/__tests__/useTaskRuns.test.ts +51 -0
  197. package/apps/web/src/features/tasks/hooks/useChangelogCodeCopy.ts +52 -0
  198. package/apps/web/src/features/tasks/hooks/useCopyToClipboard.ts +47 -0
  199. package/apps/web/src/features/tasks/hooks/useTaskChangelog.ts +32 -0
  200. package/apps/web/src/features/tasks/hooks/useTaskCreate.ts +137 -0
  201. package/apps/web/src/features/tasks/hooks/useTaskDetail.ts +217 -0
  202. package/apps/web/src/features/tasks/hooks/useTaskEdit.ts +130 -0
  203. package/apps/web/src/features/tasks/hooks/useTaskExecution.ts +137 -0
  204. package/apps/web/src/features/tasks/hooks/useTaskList.ts +159 -0
  205. package/apps/web/src/features/tasks/hooks/useTaskNotification.ts +80 -0
  206. package/apps/web/src/features/tasks/hooks/useTaskRuns.ts +32 -0
  207. package/apps/web/src/features/tasks/ui/AgentOutputPanel.tsx +203 -0
  208. package/apps/web/src/features/tasks/ui/AgentRoleSelect.tsx +97 -0
  209. package/apps/web/src/features/tasks/ui/ChangelogPanel.tsx +321 -0
  210. package/apps/web/src/features/tasks/ui/RunHistoryPanel.tsx +193 -0
  211. package/apps/web/src/features/tasks/ui/TaskCreateModal.tsx +205 -0
  212. package/apps/web/src/features/tasks/ui/TaskDetailView.tsx +413 -0
  213. package/apps/web/src/features/tasks/ui/TaskEditModal.tsx +165 -0
  214. package/apps/web/src/features/tasks/ui/TaskListModal.tsx +591 -0
  215. package/apps/web/src/features/tasks/ui/__tests__/AgentRoleSelect.test.tsx +91 -0
  216. package/apps/web/src/features/tasks/ui/__tests__/ChangelogPanel.test.tsx +94 -0
  217. package/apps/web/src/features/tasks/ui/__tests__/RunHistoryPanel.test.tsx +71 -0
  218. package/apps/web/src/features/tasks/ui/__tests__/TaskCreateModal.test.tsx +153 -0
  219. package/apps/web/src/features/tasks/ui/__tests__/TaskEditModal.test.tsx +75 -0
  220. package/apps/web/src/features/tasks/ui/__tests__/TaskListModal.test.tsx +243 -0
  221. package/apps/web/src/hooks/useWorkingDir.ts +28 -0
  222. package/apps/web/src/lib/__tests__/ansi.test.ts +88 -0
  223. package/apps/web/src/lib/ansi.ts +105 -0
  224. package/apps/web/src/lib/constants.ts +4 -0
  225. package/apps/web/src/lib/quota.ts +22 -0
  226. package/apps/web/src/lib/theme.tsx +78 -0
  227. package/apps/web/src/lib/toast.tsx +175 -0
  228. package/apps/web/src/store/agentStatusStore.ts +38 -0
  229. package/apps/web/tsconfig.json +18 -0
  230. package/apps/web/vitest.config.ts +25 -0
  231. package/apps/web/vitest.setup.ts +10 -0
  232. package/package.json +85 -0
  233. package/packages/cli/dist/commands/check.d.ts +1 -0
  234. package/packages/cli/dist/commands/check.js +89 -0
  235. package/packages/cli/dist/commands/init.d.ts +5 -0
  236. package/packages/cli/dist/commands/init.js +183 -0
  237. package/packages/cli/dist/commands/start.d.ts +4 -0
  238. package/packages/cli/dist/commands/start.js +188 -0
  239. package/packages/cli/dist/index.d.ts +2 -0
  240. package/packages/cli/dist/index.js +71 -0
  241. package/packages/cli/dist/utils/agent-tools.d.ts +28 -0
  242. package/packages/cli/dist/utils/agent-tools.js +193 -0
  243. package/packages/cli/dist/utils/project-init.d.ts +12 -0
  244. package/packages/cli/dist/utils/project-init.js +258 -0
  245. package/packages/cli/dist/utils/proxy.d.ts +8 -0
  246. package/packages/cli/dist/utils/proxy.js +138 -0
  247. package/packages/cli/package.json +30 -0
  248. package/packages/cli/src/commands/check.ts +77 -0
  249. package/packages/cli/src/commands/init.ts +209 -0
  250. package/packages/cli/src/commands/start.ts +183 -0
  251. package/packages/cli/src/index.ts +91 -0
  252. package/packages/cli/src/utils/agent-tools.ts +201 -0
  253. package/packages/cli/src/utils/project-init.ts +252 -0
  254. package/packages/cli/src/utils/proxy.ts +123 -0
  255. package/packages/cli/tsconfig.json +14 -0
  256. package/packages/eslint-config/base.mjs +31 -0
  257. package/packages/eslint-config/nest.mjs +55 -0
  258. package/packages/eslint-config/next.mjs +23 -0
  259. package/packages/eslint-config/package.json +20 -0
  260. package/packages/typescript-config/base.json +16 -0
  261. package/packages/typescript-config/nestjs.json +17 -0
  262. package/packages/typescript-config/nextjs.json +15 -0
  263. package/packages/typescript-config/package.json +11 -0
  264. package/turbo.json +28 -0
@@ -0,0 +1,64 @@
1
+ import {
2
+ Body,
3
+ Controller,
4
+ Delete,
5
+ Get,
6
+ HttpCode,
7
+ HttpStatus,
8
+ Param,
9
+ Post,
10
+ UsePipes,
11
+ ValidationPipe,
12
+ } from '@nestjs/common';
13
+
14
+ import { CodexAuthManager } from './codex-auth.manager';
15
+ import { CodexSessionManager } from './codex-session.manager';
16
+ import type { CodexSessionInfo } from './codex-session.manager';
17
+ import { ConfigureCodexAuthDto } from './dto/configure-auth.dto';
18
+
19
+ @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
20
+ @Controller('agents/codex')
21
+ export class CodexController {
22
+ constructor(
23
+ private readonly authManager: CodexAuthManager,
24
+ private readonly sessionManager: CodexSessionManager,
25
+ ) {}
26
+
27
+ /** GET /agents/codex/auth/status */
28
+ @Get('auth/status')
29
+ getAuthStatus() {
30
+ return this.authManager.getAuthStatus();
31
+ }
32
+
33
+ /** POST /agents/codex/auth/configure */
34
+ @Post('auth/configure')
35
+ @HttpCode(HttpStatus.NO_CONTENT)
36
+ configureAuth(@Body() dto: ConfigureCodexAuthDto): void {
37
+ this.authManager.saveApiKey(dto.apiKey.trim());
38
+ }
39
+
40
+ /** POST /agents/codex/sessions */
41
+ @Post('sessions')
42
+ createSession(@Body() body: { workingDirectory?: string; model?: string; reasoning?: string }): CodexSessionInfo {
43
+ return this.sessionManager.createSession(body.workingDirectory, body.model, body.reasoning);
44
+ }
45
+
46
+ /** GET /agents/codex/sessions */
47
+ @Get('sessions')
48
+ listSessions(): CodexSessionInfo[] {
49
+ return this.sessionManager.listSessions();
50
+ }
51
+
52
+ /** GET /agents/codex/sessions/:id */
53
+ @Get('sessions/:id')
54
+ getSession(@Param('id') id: string): CodexSessionInfo {
55
+ return this.sessionManager.getSession(id);
56
+ }
57
+
58
+ /** DELETE /agents/codex/sessions/:id */
59
+ @Delete('sessions/:id')
60
+ @HttpCode(HttpStatus.NO_CONTENT)
61
+ terminateSession(@Param('id') id: string): void {
62
+ this.sessionManager.terminateSession(id);
63
+ }
64
+ }
@@ -0,0 +1,97 @@
1
+ import { Logger } from '@nestjs/common';
2
+ import {
3
+ ConnectedSocket,
4
+ MessageBody,
5
+ OnGatewayDisconnect,
6
+ OnGatewayInit,
7
+ SubscribeMessage,
8
+ WebSocketGateway,
9
+ WebSocketServer,
10
+ } from '@nestjs/websockets';
11
+ import { Server, Socket } from 'socket.io';
12
+
13
+ import { CodexAuthManager } from './codex-auth.manager';
14
+ import { CodexSessionManager } from './codex-session.manager';
15
+
16
+ @WebSocketGateway({ namespace: '/agents/codex', cors: { origin: '*' } })
17
+ export class CodexGateway implements OnGatewayInit, OnGatewayDisconnect {
18
+ @WebSocketServer()
19
+ private readonly server!: Server;
20
+
21
+ private readonly logger = new Logger(CodexGateway.name);
22
+
23
+ constructor(
24
+ private readonly authManager: CodexAuthManager,
25
+ private readonly sessionManager: CodexSessionManager,
26
+ ) {}
27
+
28
+ afterInit(): void {
29
+ this.sessionManager.on('session:text', (ev) => this.server.to(ev.sessionId).emit('session:text', ev));
30
+ this.sessionManager.on('session:result', (ev) => this.server.to(ev.sessionId).emit('session:result', ev));
31
+ this.sessionManager.on('session:exit', (ev) => this.server.to(ev.sessionId).emit('session:exit', ev));
32
+ this.sessionManager.on('error', (ev) => this.server.to(ev.sessionId).emit('error', { message: ev.message }));
33
+ this.sessionManager.on('session:replaced', async (ev: { oldSessionId: string; newSessionId: string }) => {
34
+ const sockets = await this.server.in(ev.oldSessionId).fetchSockets();
35
+ await Promise.all(sockets.map((s) => s.join(ev.newSessionId)));
36
+ this.server.to(ev.newSessionId).emit('session:replaced', { oldSessionId: ev.oldSessionId, newSessionId: ev.newSessionId });
37
+ });
38
+ this.logger.log('CodexGateway initialised β€” namespace: /agents/codex');
39
+ }
40
+
41
+ handleDisconnect(client: Socket): void {
42
+ this.authManager.cancelLogin(client.id);
43
+ }
44
+
45
+ // ─── Auth ─────────────────────────────────────────────────────────────────
46
+
47
+ @SubscribeMessage('auth:login:start')
48
+ handleAuthLoginStart(@ConnectedSocket() client: Socket): void {
49
+ this.authManager.startLogin(
50
+ client.id,
51
+ (text) => client.emit('auth:output', { text }),
52
+ (success) => client.emit('auth:done', { success }),
53
+ );
54
+ }
55
+
56
+ @SubscribeMessage('auth:login:cancel')
57
+ handleAuthLoginCancel(@ConnectedSocket() client: Socket): void {
58
+ this.authManager.cancelLogin(client.id);
59
+ }
60
+
61
+ // ─── Session ──────────────────────────────────────────────────────────────
62
+
63
+ @SubscribeMessage('session:message')
64
+ handleSessionMessage(
65
+ @ConnectedSocket() client: Socket,
66
+ @MessageBody() body: { sessionId: string; input: string; model?: string; reasoning?: string },
67
+ ): void {
68
+ try {
69
+ void client.join(body.sessionId);
70
+ this.sessionManager.sendMessageWithSettings(body.sessionId, body.input, body.model, body.reasoning);
71
+ } catch (err) {
72
+ const message = err instanceof Error ? err.message : 'Unknown error';
73
+ client.emit('error', { message });
74
+ }
75
+ }
76
+
77
+ @SubscribeMessage('session:join')
78
+ handleSessionJoin(
79
+ @ConnectedSocket() client: Socket,
80
+ @MessageBody() body: { sessionId: string },
81
+ ): void {
82
+ void client.join(body.sessionId);
83
+ }
84
+
85
+ @SubscribeMessage('session:terminate')
86
+ handleSessionTerminate(
87
+ @ConnectedSocket() client: Socket,
88
+ @MessageBody() body: { sessionId: string },
89
+ ): void {
90
+ try {
91
+ this.sessionManager.terminateSession(body.sessionId);
92
+ } catch (err) {
93
+ const message = err instanceof Error ? err.message : 'Unknown error';
94
+ client.emit('error', { message });
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,17 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { TypeOrmModule } from '@nestjs/typeorm';
3
+
4
+ import { AgentSessionEntity } from '../../../database/entities/agent-session.entity';
5
+ import { SessionEntity } from '../../../database/entities/session.entity';
6
+ import { CodexAuthManager } from './codex-auth.manager';
7
+ import { CodexController } from './codex.controller';
8
+ import { CodexGateway } from './codex.gateway';
9
+ import { CodexSessionManager } from './codex-session.manager';
10
+
11
+ @Module({
12
+ imports: [TypeOrmModule.forFeature([AgentSessionEntity, SessionEntity])],
13
+ controllers: [CodexController],
14
+ providers: [CodexAuthManager, CodexSessionManager, CodexGateway],
15
+ exports: [CodexAuthManager, CodexSessionManager],
16
+ })
17
+ export class CodexModule {}
@@ -0,0 +1,7 @@
1
+ import { IsNotEmpty, IsString } from 'class-validator';
2
+
3
+ export class ConfigureCodexAuthDto {
4
+ @IsString()
5
+ @IsNotEmpty()
6
+ apiKey!: string;
7
+ }
@@ -0,0 +1,15 @@
1
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2
+ import { IsIn, IsOptional, IsString } from 'class-validator';
3
+
4
+ import type { GeminiAuthType } from '../gemini-auth.manager';
5
+
6
+ export class ConfigureAuthDto {
7
+ @ApiProperty({ enum: ['api-key', 'gca'], example: 'api-key', description: '인증 방식' })
8
+ @IsIn(['api-key', 'gca'])
9
+ authType!: GeminiAuthType;
10
+
11
+ @ApiPropertyOptional({ example: 'AIza...', description: 'API Key (authType=api-key μ‹œ ν•„μˆ˜)' })
12
+ @IsOptional()
13
+ @IsString()
14
+ apiKey?: string;
15
+ }
@@ -0,0 +1,9 @@
1
+ import { ApiPropertyOptional } from '@nestjs/swagger';
2
+ import { IsOptional, IsString } from 'class-validator';
3
+
4
+ export class CreateSessionDto {
5
+ @ApiPropertyOptional({ example: '/Users/me/my-project', description: 'μ„Έμ…˜ μž‘μ—… 디렉토리' })
6
+ @IsOptional()
7
+ @IsString()
8
+ workingDirectory?: string;
9
+ }
@@ -0,0 +1,9 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsString, MinLength } from 'class-validator';
3
+
4
+ export class SendInputDto {
5
+ @ApiProperty({ example: 'ν˜„μž¬ μ½”λ“œλ₯Ό λΆ„μ„ν•΄μ€˜', description: 'μ—μ΄μ „νŠΈμ—κ²Œ 전달할 λ©”μ‹œμ§€' })
6
+ @IsString()
7
+ @MinLength(1)
8
+ input!: string;
9
+ }
@@ -0,0 +1,157 @@
1
+ import { execFileSync, spawn } from 'child_process';
2
+ import type { ChildProcess } from 'child_process';
3
+ import * as fs from 'fs';
4
+ import * as os from 'os';
5
+ import * as path from 'path';
6
+
7
+ import { JI_PATHS } from '../../../common/ji-paths';
8
+ import { Injectable, Logger } from '@nestjs/common';
9
+
10
+ const IS_WIN = process.platform === 'win32';
11
+
12
+ export type GeminiAuthType = 'api-key' | 'gca';
13
+
14
+ export interface GeminiAuthStatus {
15
+ loggedIn: boolean;
16
+ authMethod: GeminiAuthType | 'unknown';
17
+ installed: boolean;
18
+ }
19
+
20
+ interface GeminiSettings {
21
+ selectedAuthType?: GeminiAuthType | string;
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ const SETTINGS_PATH = JI_PATHS.agents.gemini.settings;
26
+ const API_KEY_PATH = JI_PATHS.agents.gemini.apiKey;
27
+ const GCA_CREDS_PATH = path.join(
28
+ os.homedir(),
29
+ '.config',
30
+ 'gcloud',
31
+ 'application_default_credentials.json',
32
+ );
33
+
34
+ @Injectable()
35
+ export class GeminiAuthManager {
36
+ private readonly logger = new Logger(GeminiAuthManager.name);
37
+ private readonly loginProcesses = new Map<string, ChildProcess>();
38
+
39
+ // ─── Status ────────────────────────────────────────────────────────────────
40
+
41
+ async getAuthStatus(): Promise<GeminiAuthStatus> {
42
+ if (!this.isInstalled()) {
43
+ return { loggedIn: false, authMethod: 'unknown', installed: false };
44
+ }
45
+
46
+ const settings = this.readSettings();
47
+ const authType = settings?.selectedAuthType as GeminiAuthType | undefined;
48
+
49
+ if (authType === 'api-key') {
50
+ const hasKey = !!(this.readStoredApiKey() || process.env.GEMINI_API_KEY);
51
+ return { loggedIn: hasKey, authMethod: 'api-key', installed: true };
52
+ }
53
+
54
+ if (authType === 'gca') {
55
+ const hasGca = fs.existsSync(GCA_CREDS_PATH);
56
+ return { loggedIn: hasGca, authMethod: 'gca', installed: true };
57
+ }
58
+
59
+ return { loggedIn: false, authMethod: 'unknown', installed: true };
60
+ }
61
+
62
+ // ─── API Key ───────────────────────────────────────────────────────────────
63
+
64
+ saveApiKey(apiKey: string): void {
65
+ this.ensureGeminiDir();
66
+ this.writeSettings({ selectedAuthType: 'api-key' });
67
+ fs.writeFileSync(API_KEY_PATH, apiKey, { mode: 0o600 });
68
+ this.logger.log('Saved Gemini API key config');
69
+ }
70
+
71
+ getEnvForGemini(): NodeJS.ProcessEnv {
72
+ const apiKey = this.readStoredApiKey() ?? process.env.GEMINI_API_KEY;
73
+ return apiKey ? { ...process.env, GEMINI_API_KEY: apiKey } : process.env;
74
+ }
75
+
76
+ // ─── GCA Login (gcloud auth application-default login) ────────────────────
77
+
78
+ startGcaLogin(
79
+ clientId: string,
80
+ onOutput: (text: string) => void,
81
+ onDone: (success: boolean) => void,
82
+ ): void {
83
+ this.cancelLogin(clientId);
84
+
85
+ const proc = spawn(IS_WIN ? 'gcloud.cmd' : 'gcloud', ['auth', 'application-default', 'login'], {
86
+ stdio: ['ignore', 'pipe', 'pipe'],
87
+ env: process.env,
88
+ });
89
+
90
+ this.loginProcesses.set(clientId, proc);
91
+ this.logger.log(`Started gcloud auth for client ${clientId} (pid: ${proc.pid})`);
92
+
93
+ proc.stdout.on('data', (chunk: Buffer) => onOutput(chunk.toString()));
94
+ proc.stderr.on('data', (chunk: Buffer) => onOutput(chunk.toString()));
95
+
96
+ proc.on('close', (code) => {
97
+ this.loginProcesses.delete(clientId);
98
+ const success = code === 0;
99
+ if (success) this.writeSettings({ selectedAuthType: 'gca' });
100
+ this.logger.log(`gcloud auth for ${clientId} exited with code ${code}`);
101
+ onDone(success);
102
+ });
103
+
104
+ proc.on('error', (err) => {
105
+ this.loginProcesses.delete(clientId);
106
+ this.logger.error(`gcloud auth error for ${clientId}: ${err.message}`);
107
+ onDone(false);
108
+ });
109
+ }
110
+
111
+ cancelLogin(clientId: string): void {
112
+ const proc = this.loginProcesses.get(clientId);
113
+ if (proc) {
114
+ proc.kill();
115
+ this.loginProcesses.delete(clientId);
116
+ }
117
+ }
118
+
119
+ // ─── Private helpers ───────────────────────────────────────────────────────
120
+
121
+ private isInstalled(): boolean {
122
+ try {
123
+ execFileSync('gemini', ['--version'], { stdio: 'ignore', shell: true });
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ private ensureGeminiDir(): void {
131
+ if (!fs.existsSync(JI_PATHS.agents.gemini.dir)) {
132
+ fs.mkdirSync(JI_PATHS.agents.gemini.dir, { recursive: true });
133
+ }
134
+ }
135
+
136
+ private readSettings(): GeminiSettings | null {
137
+ try {
138
+ return JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8')) as GeminiSettings;
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+
144
+ private writeSettings(patch: Partial<GeminiSettings>): void {
145
+ this.ensureGeminiDir();
146
+ const current = this.readSettings() ?? {};
147
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify({ ...current, ...patch }, null, 2));
148
+ }
149
+
150
+ private readStoredApiKey(): string | null {
151
+ try {
152
+ return fs.readFileSync(API_KEY_PATH, 'utf8').trim() || null;
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+ }
@@ -0,0 +1,287 @@
1
+ import { execSync, spawn, type ChildProcess } from 'child_process';
2
+ import { EventEmitter } from 'events';
3
+ import * as fs from 'fs';
4
+
5
+ const IS_WIN = process.platform === 'win32';
6
+ import * as path from 'path';
7
+
8
+ import { Injectable, Logger, NotFoundException, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
9
+ import { InjectRepository } from '@nestjs/typeorm';
10
+ import { Repository } from 'typeorm';
11
+ import { v4 as uuidv4 } from 'uuid';
12
+
13
+ import { AgentSessionEntity } from '../../../database/entities/agent-session.entity';
14
+ import { SessionEntity } from '../../../database/entities/session.entity';
15
+ import { GeminiAuthManager } from './gemini-auth.manager';
16
+ import type { GeminiSession, SessionInfo } from './interfaces/gemini-session.interface';
17
+ import type { ResultEvent, SessionExitEvent, TextDeltaEvent } from './interfaces/stream-event.interface';
18
+
19
+ @Injectable()
20
+ export class GeminiSessionManager extends EventEmitter implements OnModuleInit, OnModuleDestroy {
21
+ private readonly logger = new Logger(GeminiSessionManager.name);
22
+ private readonly sessions = new Map<string, GeminiSession>();
23
+ private readonly processes = new Map<string, ChildProcess>();
24
+ private geminiBin = 'gemini';
25
+
26
+ constructor(
27
+ @InjectRepository(AgentSessionEntity)
28
+ private readonly agentSessionRepo: Repository<AgentSessionEntity>,
29
+ @InjectRepository(SessionEntity)
30
+ private readonly sessionRepo: Repository<SessionEntity>,
31
+ private readonly authManager: GeminiAuthManager,
32
+ ) {
33
+ super();
34
+ }
35
+
36
+ onModuleInit(): void {
37
+ this.geminiBin = this.resolveGemini();
38
+ this.logger.log(`gemini 경둜: ${this.geminiBin}`);
39
+ }
40
+
41
+ // ─── μ„Έμ…˜ 생λͺ…μ£ΌκΈ° ───────────────────────────────────────────────────
42
+
43
+ createSession(workingDirectory = process.cwd()): SessionInfo {
44
+ const id = uuidv4();
45
+ const now = new Date();
46
+ const session: GeminiSession = {
47
+ id,
48
+ status: 'idle',
49
+ workingDirectory,
50
+ createdAt: now,
51
+ lastActivity: now,
52
+ persisted: false,
53
+ };
54
+ this.sessions.set(id, session);
55
+
56
+ this.logger.log(`Created in-memory Gemini session ${id}`);
57
+ return this.toSessionInfo(session);
58
+ }
59
+
60
+ terminateSession(sessionId: string): void {
61
+ const session = this.requireSession(sessionId);
62
+ session.status = 'terminated';
63
+ this.killProcess(sessionId);
64
+ this.sessions.delete(sessionId);
65
+
66
+ if (session.persisted) {
67
+ void this.agentSessionRepo.update(sessionId, { status: 'terminated' });
68
+ }
69
+
70
+ this.logger.log(`Terminated Gemini session ${sessionId}`);
71
+ }
72
+
73
+ // ─── λ©”μ‹œμ§€ 전솑 ─────────────────────────────────────────────────────
74
+
75
+ sendMessage(sessionId: string, message: string): void {
76
+ const existing = this.sessions.get(sessionId);
77
+ if (!existing) {
78
+ void this.restoreAndSend(sessionId, message);
79
+ return;
80
+ }
81
+
82
+ if (existing.status === 'processing') {
83
+ throw new Error(`Gemini session ${sessionId} is already processing`);
84
+ }
85
+
86
+ existing.status = 'processing';
87
+ existing.lastActivity = new Date();
88
+
89
+ if (!existing.persisted) {
90
+ void this.persistSession(existing).then(() => {
91
+ if (existing.status === 'terminated' || !this.sessions.has(sessionId)) {
92
+ void this.agentSessionRepo.update(sessionId, { status: 'terminated' });
93
+ return;
94
+ }
95
+ this.spawnGemini(existing, message);
96
+ });
97
+ return;
98
+ }
99
+
100
+ void this.agentSessionRepo.update(sessionId, { status: 'processing' });
101
+ this.spawnGemini(existing, message);
102
+ }
103
+
104
+ private async restoreAndSend(sessionId: string, message: string): Promise<void> {
105
+ const record = await this.agentSessionRepo.findOne({ where: { id: sessionId } });
106
+ if (!record) {
107
+ this.emit('text-delta', { sessionId, text: '⚠ μ„Έμ…˜μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.' });
108
+ this.emit('result', { sessionId, isError: true });
109
+ return;
110
+ }
111
+ const now = new Date();
112
+ const session: GeminiSession = {
113
+ id: sessionId,
114
+ status: 'processing',
115
+ workingDirectory: record.workingDirectory,
116
+ createdAt: record.createdAt,
117
+ lastActivity: now,
118
+ persisted: true,
119
+ };
120
+ this.sessions.set(sessionId, session);
121
+ this.logger.log(`Restored Gemini session ${sessionId} (cwd: ${record.workingDirectory})`);
122
+ void this.agentSessionRepo.update(sessionId, { status: 'processing' });
123
+ this.spawnGemini(session, message);
124
+ }
125
+
126
+ // ─── 쑰회 ────────────────────────────────────────────────────────────
127
+
128
+ getSessionInfo(sessionId: string): SessionInfo {
129
+ const session = this.sessions.get(sessionId);
130
+ if (!session) throw new NotFoundException(`Gemini session ${sessionId} not found`);
131
+ return this.toSessionInfo(session);
132
+ }
133
+
134
+ listSessions(): SessionInfo[] {
135
+ return [...this.sessions.values()].map((s) => this.toSessionInfo(s));
136
+ }
137
+
138
+ // ─── 정리 ────────────────────────────────────────────────────────────
139
+
140
+ onModuleDestroy(): void {
141
+ for (const sessionId of this.processes.keys()) {
142
+ this.killProcess(sessionId);
143
+ }
144
+ for (const session of this.sessions.values()) {
145
+ session.status = 'terminated';
146
+ }
147
+ this.sessions.clear();
148
+ this.logger.log('All Gemini sessions cleared');
149
+ }
150
+
151
+ // ─── Private ─────────────────────────────────────────────────────────
152
+
153
+ private async persistSession(session: GeminiSession): Promise<void> {
154
+ const title = path.basename(session.workingDirectory) || session.workingDirectory;
155
+ await Promise.all([
156
+ this.agentSessionRepo.save({
157
+ id: session.id,
158
+ claudeSessionId: null,
159
+ status: 'processing',
160
+ workingDirectory: session.workingDirectory,
161
+ }),
162
+ this.sessionRepo.save({ sessionId: session.id, title, agentType: 'gemini' }),
163
+ ]);
164
+ session.persisted = true;
165
+ this.logger.log(`Persisted Gemini session ${session.id} (${title})`);
166
+ }
167
+
168
+ private spawnGemini(session: GeminiSession, message: string): void {
169
+ const sessionId = session.id;
170
+
171
+ this.logger.log(`[${sessionId}] spawning: ${this.geminiBin} -y -p <message>`);
172
+
173
+ const proc = spawn(this.geminiBin, ['-y', '-p', message], {
174
+ cwd: session.workingDirectory,
175
+ env: this.authManager.getEnvForGemini(),
176
+ stdio: ['ignore', 'pipe', 'pipe'],
177
+ });
178
+ this.processes.set(sessionId, proc);
179
+
180
+ // Gemini CLIλŠ” stdout/stderr λͺ¨λ‘μ— 좜λ ₯ β€” 두 슀트림 ν•©μ‚° 캑처
181
+ // ANSI μ΄μŠ€μΌ€μ΄ν”„ μ‹œν€€μŠ€(색상, μ»€μ„œ 이동 λ“±) 전체 제거
182
+ const handleChunk = (chunk: Buffer): void => {
183
+ const raw = chunk.toString();
184
+ const text = raw
185
+ .replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '') // CSI μ‹œν€€μŠ€
186
+ .replace(/\x1b\][^\x07]*\x07/g, '') // OSC μ‹œν€€μŠ€
187
+ .replace(/\x1b[()][AB]/g, '') // charset μ§€μ •
188
+ .replace(/\r/g, '');
189
+ if (!text.trim()) return;
190
+ const event: TextDeltaEvent = { sessionId, text };
191
+ this.emit('text-delta', event);
192
+ };
193
+
194
+ proc.stdout.on('data', handleChunk);
195
+ proc.stderr.on('data', handleChunk);
196
+
197
+ proc.on('close', (exitCode) => {
198
+ this.processes.delete(sessionId);
199
+ const exitEvent: SessionExitEvent = { sessionId, exitCode: exitCode ?? -1 };
200
+
201
+ if (session.status === 'terminated' || !this.sessions.has(sessionId)) {
202
+ this.emit('exit', exitEvent);
203
+ return;
204
+ }
205
+
206
+ this.logger.log(`[${sessionId}] gemini exited with code ${exitCode}`);
207
+ session.status = 'idle';
208
+ void this.agentSessionRepo.update(sessionId, { status: 'idle' });
209
+
210
+ const resultEvent: ResultEvent = { sessionId, isError: (exitCode ?? 0) !== 0 };
211
+ this.emit('result', resultEvent);
212
+
213
+ this.emit('exit', exitEvent);
214
+ });
215
+
216
+ proc.on('error', (err) => {
217
+ this.processes.delete(sessionId);
218
+ if (session.status === 'terminated' || !this.sessions.has(sessionId)) {
219
+ return;
220
+ }
221
+
222
+ this.logger.error(`[${sessionId}] spawn error: ${err.message}`);
223
+ session.status = 'idle';
224
+ void this.agentSessionRepo.update(sessionId, { status: 'idle' });
225
+ // μ—λŸ¬λ„ ν…μŠ€νŠΈλ‘œ μ „λ‹¬ν•΄μ„œ UI에 ν‘œμ‹œ
226
+ this.emit('text-delta', { sessionId, text: `\n⚠ μ‹€ν–‰ 였λ₯˜: ${err.message}\n` } as TextDeltaEvent);
227
+ this.emit('result', { sessionId, isError: true } as ResultEvent);
228
+ });
229
+ }
230
+
231
+ private resolveGemini(): string {
232
+ const whichCmd = IS_WIN ? 'where gemini' : 'which gemini';
233
+ const candidates = [
234
+ (): string => execSync(whichCmd, { encoding: 'utf8', timeout: 2000 }).trim().split(/\r?\n/)[0],
235
+ (): string => {
236
+ const home = IS_WIN ? (process.env.USERPROFILE ?? '') : (process.env.HOME ?? '');
237
+ const p = IS_WIN
238
+ ? `${home}\\AppData\\Roaming\\npm\\gemini.cmd`
239
+ : `${home}/.nvm/versions/node/${process.version}/bin/gemini`;
240
+ if (fs.existsSync(p)) return p;
241
+ throw new Error('not found');
242
+ },
243
+ (): string => {
244
+ const npmBin = execSync('npm bin -g', { encoding: 'utf8', timeout: 2000 }).trim();
245
+ const p = IS_WIN ? `${npmBin}\\gemini.cmd` : `${npmBin}/gemini`;
246
+ if (fs.existsSync(p)) return p;
247
+ throw new Error('not found');
248
+ },
249
+ ];
250
+
251
+ for (const fn of candidates) {
252
+ try { const p = fn(); if (p) return p; } catch {}
253
+ }
254
+
255
+ this.logger.warn('gemini 경둜 탐지 μ‹€νŒ¨ β€” "gemini"둜 폴백');
256
+ return IS_WIN ? 'gemini.cmd' : 'gemini';
257
+ }
258
+
259
+ private requireSession(sessionId: string): GeminiSession {
260
+ const session = this.sessions.get(sessionId);
261
+ if (!session) throw new NotFoundException(`Gemini session ${sessionId} not found`);
262
+ if (session.status === 'terminated') throw new Error(`Gemini session ${sessionId} is terminated`);
263
+ return session;
264
+ }
265
+
266
+ private killProcess(sessionId: string): void {
267
+ const proc = this.processes.get(sessionId);
268
+ if (!proc) return;
269
+
270
+ this.processes.delete(sessionId);
271
+ try {
272
+ if (!proc.killed) proc.kill('SIGTERM');
273
+ } catch (err) {
274
+ this.logger.warn(`Failed to kill Gemini session ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
275
+ }
276
+ }
277
+
278
+ private toSessionInfo(session: GeminiSession): SessionInfo {
279
+ return {
280
+ id: session.id,
281
+ status: session.status,
282
+ workingDirectory: session.workingDirectory,
283
+ createdAt: session.createdAt,
284
+ lastActivity: session.lastActivity,
285
+ };
286
+ }
287
+ }