kcg-code 0.0.0-stage → 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 (307) hide show
  1. package/README.md +230 -2
  2. package/bin/kcgcode.ts +13 -0
  3. package/bun-env.d.ts +19 -0
  4. package/kcg-code.config.example.json +3 -0
  5. package/package.json +84 -4
  6. package/public/logo.svg +1 -0
  7. package/public/manifest.json +18 -0
  8. package/public/sw.js +49 -0
  9. package/src/App.tsx +39 -0
  10. package/src/__tests__/manifest.test.ts +61 -0
  11. package/src/build-config.ts +9 -0
  12. package/src/cli/__tests__/daemon.test.ts +84 -0
  13. package/src/cli/__tests__/dashboard.test.ts +224 -0
  14. package/src/cli/__tests__/keep-awake.test.ts +44 -0
  15. package/src/cli/__tests__/port.test.ts +86 -0
  16. package/src/cli/args.ts +186 -0
  17. package/src/cli/attach.ts +154 -0
  18. package/src/cli/commands.ts +327 -0
  19. package/src/cli/daemon.ts +275 -0
  20. package/src/cli/dashboard.tsx +1060 -0
  21. package/src/cli/index.ts +47 -0
  22. package/src/cli/keep-awake.ts +148 -0
  23. package/src/cli/port.ts +92 -0
  24. package/src/cli/theme.ts +100 -0
  25. package/src/components/auth/AuthGate.tsx +182 -0
  26. package/src/components/auth/AvatarEditor.tsx +236 -0
  27. package/src/components/auth/LockScreen.tsx +714 -0
  28. package/src/components/auth/ProfileAvatar.tsx +60 -0
  29. package/src/components/auth/WelcomeScreen.tsx +672 -0
  30. package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
  31. package/src/components/auth/avatar-icons.tsx +114 -0
  32. package/src/components/layout/AppShell.tsx +147 -0
  33. package/src/components/layout/AppSidebar.tsx +419 -0
  34. package/src/components/layout/SearchDialog.tsx +189 -0
  35. package/src/components/layout/SidebarAccount.tsx +111 -0
  36. package/src/components/layout/SidebarSessionMenu.tsx +163 -0
  37. package/src/components/layout/ThemeToggle.tsx +67 -0
  38. package/src/components/projects/FolderBrowser.tsx +142 -0
  39. package/src/components/projects/HomeChat.tsx +287 -0
  40. package/src/components/projects/HomeRecentSessions.tsx +90 -0
  41. package/src/components/projects/NewProjectDialog.tsx +249 -0
  42. package/src/components/projects/ProjectInstructions.tsx +218 -0
  43. package/src/components/projects/ProjectRow.tsx +58 -0
  44. package/src/components/projects/ProjectSidePanel.tsx +514 -0
  45. package/src/components/sessions/AgentLogo.tsx +42 -0
  46. package/src/components/sessions/AgentModeList.tsx +265 -0
  47. package/src/components/sessions/AgentPicker.tsx +132 -0
  48. package/src/components/sessions/AssistantTurn.tsx +145 -0
  49. package/src/components/sessions/AttachmentTile.tsx +161 -0
  50. package/src/components/sessions/CodeBlock.tsx +107 -0
  51. package/src/components/sessions/ComposerActionSheet.tsx +96 -0
  52. package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
  53. package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +92 -0
  54. package/src/components/sessions/ContextPanel.tsx +661 -0
  55. package/src/components/sessions/DropPlate.tsx +165 -0
  56. package/src/components/sessions/FileDropOverlay.tsx +232 -0
  57. package/src/components/sessions/FileMentionDropdown.tsx +47 -0
  58. package/src/components/sessions/ImageLightbox.tsx +173 -0
  59. package/src/components/sessions/MarkdownContent.tsx +187 -0
  60. package/src/components/sessions/Mascot.tsx +47 -0
  61. package/src/components/sessions/MessageActions.tsx +60 -0
  62. package/src/components/sessions/MessageRail.tsx +121 -0
  63. package/src/components/sessions/ModelPicker.tsx +247 -0
  64. package/src/components/sessions/ModelSearchList.tsx +231 -0
  65. package/src/components/sessions/NewSessionDialog.tsx +172 -0
  66. package/src/components/sessions/PendingAttachments.tsx +127 -0
  67. package/src/components/sessions/PromptCard.tsx +1085 -0
  68. package/src/components/sessions/PromptPanel.tsx +112 -0
  69. package/src/components/sessions/SessionComposer.tsx +641 -0
  70. package/src/components/sessions/SessionHeader.tsx +176 -0
  71. package/src/components/sessions/SessionList.tsx +503 -0
  72. package/src/components/sessions/SessionRow.tsx +237 -0
  73. package/src/components/sessions/SessionTimeline.tsx +275 -0
  74. package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
  75. package/src/components/sessions/SessionView.tsx +240 -0
  76. package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
  77. package/src/components/sessions/UserMessage.tsx +177 -0
  78. package/src/components/sessions/__tests__/ContextPanel.test.tsx +256 -0
  79. package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
  80. package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
  81. package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
  82. package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
  83. package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
  84. package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
  85. package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
  86. package/src/components/settings/AboutGroup.tsx +170 -0
  87. package/src/components/settings/AccountGroup.tsx +122 -0
  88. package/src/components/settings/DevicesGroup.tsx +151 -0
  89. package/src/components/settings/LhrRemoteAccess.tsx +257 -0
  90. package/src/components/settings/PasskeysGroup.tsx +551 -0
  91. package/src/components/settings/RemoteAccessGroup.tsx +1201 -0
  92. package/src/components/settings/SecurityGroup.tsx +566 -0
  93. package/src/components/settings/WorkspaceGroup.tsx +370 -0
  94. package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
  95. package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
  96. package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
  97. package/src/components/settings/prefs.tsx +333 -0
  98. package/src/components/skills/InstallNotifications.tsx +326 -0
  99. package/src/components/skills/InstallSkillDialog.tsx +390 -0
  100. package/src/components/skills/SkillCards.tsx +254 -0
  101. package/src/components/skills/SkillsProjectPicker.tsx +178 -0
  102. package/src/components/ui/adw-dialog.tsx +114 -0
  103. package/src/components/ui/adw-toggle-group.tsx +75 -0
  104. package/src/components/ui/alert-dialog.tsx +179 -0
  105. package/src/components/ui/alert.tsx +60 -0
  106. package/src/components/ui/badge.tsx +46 -0
  107. package/src/components/ui/bubble.tsx +125 -0
  108. package/src/components/ui/button.tsx +62 -0
  109. package/src/components/ui/card.tsx +75 -0
  110. package/src/components/ui/collapsible.tsx +19 -0
  111. package/src/components/ui/dialog.tsx +142 -0
  112. package/src/components/ui/dropdown-menu.tsx +225 -0
  113. package/src/components/ui/empty.tsx +94 -0
  114. package/src/components/ui/field.tsx +234 -0
  115. package/src/components/ui/input-group.tsx +161 -0
  116. package/src/components/ui/input.tsx +21 -0
  117. package/src/components/ui/label.tsx +19 -0
  118. package/src/components/ui/message-scroller.tsx +131 -0
  119. package/src/components/ui/message.tsx +85 -0
  120. package/src/components/ui/popover.tsx +46 -0
  121. package/src/components/ui/search-entry.tsx +70 -0
  122. package/src/components/ui/select.tsx +172 -0
  123. package/src/components/ui/separator.tsx +28 -0
  124. package/src/components/ui/sheet.tsx +138 -0
  125. package/src/components/ui/sidebar.tsx +699 -0
  126. package/src/components/ui/skeleton.tsx +15 -0
  127. package/src/components/ui/sonner.tsx +45 -0
  128. package/src/components/ui/spinner.tsx +16 -0
  129. package/src/components/ui/switch.tsx +32 -0
  130. package/src/components/ui/textarea.tsx +18 -0
  131. package/src/components/ui/tooltip.tsx +51 -0
  132. package/src/config.ts +114 -0
  133. package/src/db.ts +146 -0
  134. package/src/frontend.tsx +51 -0
  135. package/src/hooks/__tests__/useMention.test.ts +79 -0
  136. package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
  137. package/src/hooks/__tests__/useRouter.test.ts +17 -0
  138. package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
  139. package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
  140. package/src/hooks/__tests__/useTheme.test.ts +118 -0
  141. package/src/hooks/use-mobile.ts +16 -0
  142. package/src/hooks/useAgentPicker.ts +101 -0
  143. package/src/hooks/useMediaQuery.ts +49 -0
  144. package/src/hooks/useMention.ts +236 -0
  145. package/src/hooks/useProjects.ts +88 -0
  146. package/src/hooks/useResyncOnReconnect.ts +28 -0
  147. package/src/hooks/useRouter.ts +92 -0
  148. package/src/hooks/useSessionChat.ts +427 -0
  149. package/src/hooks/useSessionContext.ts +106 -0
  150. package/src/hooks/useSessionList.ts +327 -0
  151. package/src/hooks/useSidebarData.ts +108 -0
  152. package/src/hooks/useSmoothText.ts +122 -0
  153. package/src/hooks/useTheme.ts +164 -0
  154. package/src/hooks/useWebSocket.ts +196 -0
  155. package/src/index.css +27 -0
  156. package/src/index.html +14 -0
  157. package/src/index.ts +49 -0
  158. package/src/lib/__tests__/attachments.test.ts +58 -0
  159. package/src/lib/__tests__/avatar-presets.test.ts +25 -0
  160. package/src/lib/__tests__/collapsible.test.ts +101 -0
  161. package/src/lib/__tests__/composer.test.ts +118 -0
  162. package/src/lib/__tests__/highlight.test.ts +42 -0
  163. package/src/lib/__tests__/home-chat.test.ts +64 -0
  164. package/src/lib/__tests__/lock-screen.test.ts +64 -0
  165. package/src/lib/__tests__/model-picker.test.ts +205 -0
  166. package/src/lib/__tests__/onboarding.test.ts +41 -0
  167. package/src/lib/__tests__/permission-info.test.ts +61 -0
  168. package/src/lib/__tests__/project-extensions.test.ts +83 -0
  169. package/src/lib/__tests__/project-overview.test.ts +188 -0
  170. package/src/lib/__tests__/references.test.ts +121 -0
  171. package/src/lib/__tests__/routes.test.ts +134 -0
  172. package/src/lib/__tests__/session-selection.test.ts +41 -0
  173. package/src/lib/__tests__/session-summary.test.ts +115 -0
  174. package/src/lib/__tests__/session-title.test.ts +58 -0
  175. package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
  176. package/src/lib/__tests__/skill-installs.test.ts +135 -0
  177. package/src/lib/__tests__/skills-catalog.test.ts +124 -0
  178. package/src/lib/__tests__/tone.test.ts +37 -0
  179. package/src/lib/__tests__/turns-delta.test.ts +55 -0
  180. package/src/lib/agent-types.ts +49 -0
  181. package/src/lib/api.ts +306 -0
  182. package/src/lib/attachments.ts +203 -0
  183. package/src/lib/auth.ts +92 -0
  184. package/src/lib/avatar-presets.ts +31 -0
  185. package/src/lib/collapsible.ts +75 -0
  186. package/src/lib/composer.ts +89 -0
  187. package/src/lib/data-events.ts +17 -0
  188. package/src/lib/headlines.ts +30 -0
  189. package/src/lib/highlight.ts +186 -0
  190. package/src/lib/layout.ts +20 -0
  191. package/src/lib/lock-screen.ts +54 -0
  192. package/src/lib/model-picker.ts +133 -0
  193. package/src/lib/onboarding.ts +38 -0
  194. package/src/lib/passkeys.ts +113 -0
  195. package/src/lib/pending-prompt.ts +53 -0
  196. package/src/lib/permission-info.ts +101 -0
  197. package/src/lib/project-extensions.ts +136 -0
  198. package/src/lib/project-overview.ts +123 -0
  199. package/src/lib/prompts.ts +87 -0
  200. package/src/lib/references.ts +106 -0
  201. package/src/lib/routes.ts +124 -0
  202. package/src/lib/session-handoff.ts +25 -0
  203. package/src/lib/session-selection.ts +40 -0
  204. package/src/lib/session-status.ts +45 -0
  205. package/src/lib/session-summary.ts +68 -0
  206. package/src/lib/session-title.ts +63 -0
  207. package/src/lib/sidebar-groups.ts +78 -0
  208. package/src/lib/skill-installs.ts +362 -0
  209. package/src/lib/skills-catalog.ts +138 -0
  210. package/src/lib/time.ts +22 -0
  211. package/src/lib/tone.ts +80 -0
  212. package/src/lib/turns.ts +320 -0
  213. package/src/lib/utils.ts +6 -0
  214. package/src/logo.svg +31 -0
  215. package/src/pages/projects/ProjectDetailPage.tsx +131 -0
  216. package/src/pages/projects/ProjectsPage.tsx +97 -0
  217. package/src/pages/sessions/SessionPage.tsx +122 -0
  218. package/src/pages/settings/SettingsPage.tsx +366 -0
  219. package/src/pages/settings/__tests__/SettingsPage.test.ts +16 -0
  220. package/src/pages/skills/SkillsPage.tsx +745 -0
  221. package/src/paths.ts +121 -0
  222. package/src/runtime.ts +24 -0
  223. package/src/server/__tests__/app.test.ts +1044 -0
  224. package/src/server/__tests__/auth.test.ts +370 -0
  225. package/src/server/__tests__/config.test.ts +101 -0
  226. package/src/server/__tests__/db.test.ts +485 -0
  227. package/src/server/__tests__/host.test.ts +14 -0
  228. package/src/server/__tests__/tunnel-config.test.ts +38 -0
  229. package/src/server/app.ts +427 -0
  230. package/src/server/db/auth.ts +311 -0
  231. package/src/server/db/messages.ts +50 -0
  232. package/src/server/db/projects.ts +106 -0
  233. package/src/server/db/prompts.ts +78 -0
  234. package/src/server/db/rows.ts +150 -0
  235. package/src/server/db/schema.ts +221 -0
  236. package/src/server/db/sessions.ts +239 -0
  237. package/src/server/db/tunnel.ts +124 -0
  238. package/src/server/host.ts +12 -0
  239. package/src/server/result.ts +15 -0
  240. package/src/server/routes/auth-guard.ts +156 -0
  241. package/src/server/routes/auth.routes.ts +287 -0
  242. package/src/server/routes/helpers.ts +165 -0
  243. package/src/server/routes/meta.routes.ts +24 -0
  244. package/src/server/routes/passkeys.routes.ts +161 -0
  245. package/src/server/routes/projects.routes.ts +167 -0
  246. package/src/server/routes/sessions.routes.ts +197 -0
  247. package/src/server/routes/skills.routes.ts +136 -0
  248. package/src/server/routes/tunnel.routes.ts +132 -0
  249. package/src/server/routes/types.ts +35 -0
  250. package/src/server/routes/uploads.routes.ts +54 -0
  251. package/src/server/routes/workspace.routes.ts +127 -0
  252. package/src/server/services/__tests__/app-info.test.ts +18 -0
  253. package/src/server/services/__tests__/attachments.test.ts +159 -0
  254. package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
  255. package/src/server/services/__tests__/auth.test.ts +341 -0
  256. package/src/server/services/__tests__/claude-code.test.ts +609 -0
  257. package/src/server/services/__tests__/claude-mcp.test.ts +90 -0
  258. package/src/server/services/__tests__/claude-skills.test.ts +63 -0
  259. package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
  260. package/src/server/services/__tests__/git-info.test.ts +74 -0
  261. package/src/server/services/__tests__/opencode.test.ts +320 -0
  262. package/src/server/services/__tests__/project-manager.test.ts +190 -0
  263. package/src/server/services/__tests__/sandbox.test.ts +141 -0
  264. package/src/server/services/__tests__/session-manager.test.ts +3509 -0
  265. package/src/server/services/__tests__/skill-install-jobs.test.ts +245 -0
  266. package/src/server/services/__tests__/skills-registry.test.ts +365 -0
  267. package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
  268. package/src/server/services/__tests__/tunnel.test.ts +367 -0
  269. package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
  270. package/src/server/services/__tests__/workspace.test.ts +249 -0
  271. package/src/server/services/app-info.ts +29 -0
  272. package/src/server/services/attachments.ts +193 -0
  273. package/src/server/services/auth-passkeys.ts +459 -0
  274. package/src/server/services/auth.ts +633 -0
  275. package/src/server/services/claude-code-client.ts +1093 -0
  276. package/src/server/services/claude-code-translate.ts +370 -0
  277. package/src/server/services/claude-mcp.ts +122 -0
  278. package/src/server/services/claude-skills.ts +102 -0
  279. package/src/server/services/frpc-installer.ts +183 -0
  280. package/src/server/services/git-info.ts +87 -0
  281. package/src/server/services/opencode-client.ts +714 -0
  282. package/src/server/services/opencode-server.ts +319 -0
  283. package/src/server/services/opencode-sse.ts +82 -0
  284. package/src/server/services/project-manager.ts +147 -0
  285. package/src/server/services/sandbox.ts +83 -0
  286. package/src/server/services/session-events.ts +256 -0
  287. package/src/server/services/session-manager.ts +1439 -0
  288. package/src/server/services/skill-install-jobs.ts +275 -0
  289. package/src/server/services/skills-registry.ts +480 -0
  290. package/src/server/services/tunnel-api.ts +207 -0
  291. package/src/server/services/tunnel-config.ts +45 -0
  292. package/src/server/services/tunnel-lhr.ts +387 -0
  293. package/src/server/services/tunnel.ts +633 -0
  294. package/src/server/services/turn-stream.ts +339 -0
  295. package/src/server/services/websocket-gateway.ts +374 -0
  296. package/src/server/services/workspace.ts +223 -0
  297. package/src/server/services/ws-transport.ts +121 -0
  298. package/src/types/context.ts +69 -0
  299. package/src/types/index.ts +13 -0
  300. package/src/types/message.ts +58 -0
  301. package/src/types/project.ts +16 -0
  302. package/src/types/prompt.ts +118 -0
  303. package/src/types/session.ts +52 -0
  304. package/src/types/turns.ts +47 -0
  305. package/src/ws-protocol.ts +93 -0
  306. package/styles/globals.css +311 -0
  307. package/tsconfig.json +37 -0
@@ -0,0 +1,714 @@
1
+ /**
2
+ * OpenCode_Client — client HTTP + SSE ke server headless `opencode serve`.
3
+ *
4
+ * Menggantikan `pty-process.ts` (PTY/TUI): alih-alih menulis byte ke terminal,
5
+ * percakapan dilakukan lewat API terstruktur:
6
+ * - `POST /session` buat session (`ses_...`)
7
+ * - `POST /session/{id}/message` kirim prompt, balasan terstruktur
8
+ * - `POST /session/{id}/prompt_async` kirim prompt, balas 204 (hasil via SSE)
9
+ * - `POST /permission/{id}/reply` jawab izin tool (`once|always|reject`)
10
+ * - `POST /question/{id}/reply|reject` jawab pertanyaan
11
+ * - `POST /session/{id}/abort` hentikan turn berjalan
12
+ * - `GET /event` SSE — event bertipe (permission.asked,
13
+ * question.asked, message.*, session.*)
14
+ *
15
+ * `subscribeEvents` menormalkan frame SSE `{ id, type, properties }` menjadi
16
+ * `{ type, id, ...properties }` agar konsumen mudah membaca `requestID`,
17
+ * `sessionID`, `permission`, `questions`, dst.
18
+ */
19
+ import type { McpServerState, MessagePart, SessionModel } from "../../types";
20
+ import type { Result, SimpleResult } from "../result";
21
+ import { subscribeOpenCodeEvents } from "./opencode-sse";
22
+
23
+ export interface OpenCodeSessionInfo {
24
+ id: string;
25
+ directory?: string;
26
+ /** Judul session; opencode meng-generate ulang setelah prompt pertama. */
27
+ title?: string;
28
+ }
29
+
30
+ /** Satu model yang tersedia pada sebuah provider (hasil GET /config/providers). */
31
+ export interface ModelOption {
32
+ providerID: string;
33
+ providerName: string;
34
+ modelID: string;
35
+ /** Nama tampilan (fallback: modelID). */
36
+ name: string;
37
+ }
38
+
39
+ /** Satu agent (mode) opencode — hasil `GET /agent` server headless. */
40
+ export interface AgentOption {
41
+ name: string;
42
+ mode: "primary" | "subagent" | "all";
43
+ description: string | null;
44
+ }
45
+
46
+ /**
47
+ * Satu MCP server Project — hasil `GET /mcp` server headless. Bentuknya
48
+ * didefinisikan SEKALI di `types/context.ts` (dipakai juga UI) agar server &
49
+ * klien tidak pernah berbeda.
50
+ */
51
+ export type McpServerInfo = McpServerState;
52
+
53
+ /** Status koneksi MCP server — mengikuti skema `MCPStatus` opencode. */
54
+ export type McpStatus = McpServerState["status"];
55
+
56
+ /** Asal sebuah skill: bawaan opencode, folder Project, atau global user. */
57
+ export type SkillSource = "builtin" | "project" | "global";
58
+
59
+ /** Satu skill yang terdaftar — hasil `GET /skill` (tanpa isi `content`). */
60
+ export interface SkillInfo {
61
+ name: string;
62
+ description: string | null;
63
+ /** Path SKILL.md; null untuk skill bawaan. */
64
+ location: string | null;
65
+ source: SkillSource;
66
+ }
67
+
68
+ const MCP_STATUSES: readonly McpStatus[] = [
69
+ "connected",
70
+ "disabled",
71
+ "failed",
72
+ "needs_auth",
73
+ "needs_client_registration",
74
+ ];
75
+
76
+ /**
77
+ * Flatten respons `GET /mcp` (`{ [name]: { status, error? } }`) menjadi
78
+ * daftar terurut nama. Status tak dikenal dipetakan ke `unknown`.
79
+ */
80
+ export function flattenMcp(payload: unknown): McpServerInfo[] {
81
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return [];
82
+ const out: McpServerInfo[] = [];
83
+ for (const [name, raw] of Object.entries(payload as Record<string, unknown>)) {
84
+ if (name === "") continue;
85
+ const v = (typeof raw === "object" && raw !== null ? raw : {}) as {
86
+ status?: unknown;
87
+ error?: unknown;
88
+ };
89
+ const status = MCP_STATUSES.includes(v.status as McpStatus)
90
+ ? (v.status as McpStatus)
91
+ : "unknown";
92
+ out.push({
93
+ name,
94
+ status,
95
+ error: typeof v.error === "string" && v.error !== "" ? v.error : null,
96
+ });
97
+ }
98
+ return out.sort((a, b) => a.name.localeCompare(b.name));
99
+ }
100
+
101
+ /**
102
+ * Flatten respons `GET /skill` menjadi daftar ringkas. `content` (isi
103
+ * SKILL.md, bisa panjang) sengaja dibuang. `projectPath` dipakai untuk
104
+ * mengklasifikasi asal skill: di dalam direktori Project -> `project`,
105
+ * `<built-in>` -> `builtin`, selain itu -> `global`.
106
+ */
107
+ export function flattenSkills(payload: unknown, projectPath?: string): SkillInfo[] {
108
+ if (!Array.isArray(payload)) return [];
109
+ const root = projectPath ? projectPath.replace(/\/+$/, "") : null;
110
+ const out: SkillInfo[] = [];
111
+ for (const raw of payload) {
112
+ if (typeof raw !== "object" || raw === null) continue;
113
+ const s = raw as { name?: unknown; description?: unknown; location?: unknown };
114
+ if (typeof s.name !== "string" || s.name === "") continue;
115
+ const loc = typeof s.location === "string" && s.location !== "" ? s.location : null;
116
+ let source: SkillSource = "global";
117
+ if (loc === null || loc === "<built-in>") source = "builtin";
118
+ else if (root && (loc === root || loc.startsWith(`${root}/`))) source = "project";
119
+ out.push({
120
+ name: s.name,
121
+ description: typeof s.description === "string" && s.description !== "" ? s.description : null,
122
+ location: source === "builtin" ? null : loc,
123
+ source,
124
+ });
125
+ }
126
+ return out.sort((a, b) => a.name.localeCompare(b.name));
127
+ }
128
+
129
+ /**
130
+ * Flatten respons `GET /agent` menjadi opsi siap pakai UI: hanya agent
131
+ * primary/all (subagent murni tidak bisa dipakai sebagai mode percakapan).
132
+ * Diekspor agar dapat diuji tanpa server.
133
+ */
134
+ export function flattenAgents(payload: unknown): AgentOption[] {
135
+ if (!Array.isArray(payload)) return [];
136
+ const out: AgentOption[] = [];
137
+ for (const raw of payload) {
138
+ if (typeof raw !== "object" || raw === null) continue;
139
+ const a = raw as { name?: unknown; mode?: unknown; description?: unknown };
140
+ if (typeof a.name !== "string" || a.name === "") continue;
141
+ if (a.mode === "subagent") continue; // bukan mode percakapan
142
+ out.push({
143
+ name: a.name,
144
+ mode: a.mode === "all" ? "all" : "primary",
145
+ description: typeof a.description === "string" && a.description !== "" ? a.description : null,
146
+ });
147
+ }
148
+ return out;
149
+ }
150
+
151
+ /**
152
+ * Flatten respons `GET /config/providers` menjadi satu entri per model.
153
+ * Bentuk sumber: `{ providers: [{ id, name, models: { [modelID]: {...} } }] }`.
154
+ * Model berstatus `deprecated` disaring; provider tanpa model diabaikan.
155
+ * Diekspor agar dapat diuji tanpa server.
156
+ */
157
+ export function flattenProviders(payload: unknown): ModelOption[] {
158
+ const providers = (payload as { providers?: unknown } | null)?.providers;
159
+ if (!Array.isArray(providers)) return [];
160
+ const out: ModelOption[] = [];
161
+ for (const p of providers) {
162
+ if (typeof p !== "object" || p === null) continue;
163
+ const prov = p as { id?: unknown; name?: unknown; models?: unknown };
164
+ if (typeof prov.id !== "string" || prov.id === "") continue;
165
+ const providerName = typeof prov.name === "string" && prov.name !== "" ? prov.name : prov.id;
166
+ const models =
167
+ typeof prov.models === "object" && prov.models !== null
168
+ ? (prov.models as Record<string, unknown>)
169
+ : {};
170
+ for (const [modelID, raw] of Object.entries(models)) {
171
+ const m = (typeof raw === "object" && raw !== null ? raw : {}) as {
172
+ name?: unknown;
173
+ status?: unknown;
174
+ };
175
+ if (m.status === "deprecated") continue;
176
+ out.push({
177
+ providerID: prov.id,
178
+ providerName,
179
+ modelID,
180
+ name: typeof m.name === "string" && m.name !== "" ? m.name : modelID,
181
+ });
182
+ }
183
+ }
184
+ return out;
185
+ }
186
+
187
+ export interface OpenCodeMessageResult {
188
+ info: { id: string; role: string; [k: string]: unknown };
189
+ parts: MessagePart[];
190
+ }
191
+
192
+ export type OpenCodePermissionReply = "once" | "always" | "reject";
193
+
194
+ /** Referensi file untuk part `file` prompt: `@file` teks maupun gambar. */
195
+ export interface OpenCodeFileRef {
196
+ /** Nama tampilan part (path relatif project / nama file asli upload). */
197
+ filename: string;
198
+ /** MIME part — `text/plain` untuk @file, `image/*` untuk gambar. */
199
+ mime: string;
200
+ /** URL absolut yang dibaca opencode (`file:///abs/path`). */
201
+ url: string;
202
+ }
203
+
204
+ /**
205
+ * Satu item task list (`todowrite` opencode) — `GET /session/{id}/todo`.
206
+ * `status`/`priority` dibiarkan string: skema opencode mendeskripsikannya
207
+ * sebagai string bebas (pending/in_progress/completed/cancelled), jadi nilai
208
+ * baru dari versi opencode berikutnya tidak membuat parsing gagal.
209
+ */
210
+ export interface OpenCodeTodo {
211
+ content: string;
212
+ status: string;
213
+ priority: string;
214
+ }
215
+
216
+ /** Satu file yang berubah di worktree — `GET /file/status`. */
217
+ export interface OpenCodeFileChange {
218
+ path: string;
219
+ added: number;
220
+ removed: number;
221
+ status: "added" | "deleted" | "modified";
222
+ }
223
+
224
+ /** Satu event SSE hasil normalisasi (properties digabung ke level atas). */
225
+ export interface OpenCodeEvent {
226
+ type: string;
227
+ id?: string;
228
+ [k: string]: unknown;
229
+ }
230
+
231
+ export interface OpenCodeClient {
232
+ /**
233
+ * Buat Session di server headless. Tanpa `title` (disarankan) opencode
234
+ * meng-generate judul dari prompt pertama; dengan `title`, judul tetap.
235
+ */
236
+ createSession(opts?: { title?: string }): Promise<Result<OpenCodeSessionInfo>>;
237
+ /**
238
+ * Cek keberadaan Session di server headless (`GET /session/{id}`):
239
+ * `{ ok: true }` bila Session masih dikenal server, `{ ok: false }`
240
+ * bila tidak ada / server gagal merespons.
241
+ */
242
+ getSession(sessionId: string): Promise<SimpleResult>;
243
+ /** Daftar model yang tersedia pada server (`GET /config/providers`),
244
+ * sudah di-flatten menjadi satu entri per model.
245
+ */
246
+ listModels(): Promise<Result<ModelOption[]>>;
247
+ /**
248
+ * Daftar agent (mode) pada server (`GET /agent`) — hanya primary/all;
249
+ * subagent murni tidak bisa dipakai sebagai mode percakapan.
250
+ */
251
+ listAgents(): Promise<Result<AgentOption[]>>;
252
+ /** Status MCP server yang terkonfigurasi (`GET /mcp`). */
253
+ listMcp(): Promise<Result<McpServerInfo[]>>;
254
+ /**
255
+ * Skill yang terdaftar (`GET /skill`). `projectPath` untuk menandai skill
256
+ * yang berasal dari folder Project.
257
+ */
258
+ listSkills(projectPath?: string): Promise<Result<SkillInfo[]>>;
259
+ /**
260
+ * Buang cache instance Project di server (`POST /instance/dispose`) agar
261
+ * skill/config dibaca ulang dari disk — `GET /skill` di-cache per
262
+ * instance. Instance dibuat ulang otomatis pada request berikutnya.
263
+ */
264
+ disposeInstance(): Promise<SimpleResult>;
265
+ /**
266
+ * Hapus Session di server headless (`DELETE /session/{id}`) beserta
267
+ * seluruh riwayat pesannya di sisi opencode.
268
+ */
269
+ deleteSession(sessionId: string): Promise<SimpleResult>;
270
+ sendMessage(
271
+ sessionId: string,
272
+ text: string,
273
+ signal?: AbortSignal,
274
+ ): Promise<Result<OpenCodeMessageResult>>;
275
+ /**
276
+ * Kirim prompt tanpa menunggu balasan (`POST /session/{id}/prompt_async`,
277
+ * 204). `model` disertakan agar Session memakai model pilihan user, bukan
278
+ * model default opencode. `files` berupa referensi file (teks `@file`
279
+ * maupun gambar) — opencode mem-parse `url` part file dengan `URL()` dan
280
+ * membaca isinya sendiri, sehingga `url` harus path absolut
281
+ * (`file:///abs/path`).
282
+ */
283
+ promptAsync(
284
+ sessionId: string,
285
+ text: string,
286
+ model?: SessionModel | null,
287
+ files?: OpenCodeFileRef[],
288
+ agent?: string | null,
289
+ /** Instruksi tambahan (custom instruction Project) -> body `system`. */
290
+ system?: string | null,
291
+ ): Promise<Result<null>>;
292
+ /** Cari file project untuk autocomplete `@file` (path relatif). */
293
+ findFiles(query: string): Promise<Result<string[]>>;
294
+ /** Task list Session (`todowrite`) — kosong bila agent belum memakainya. */
295
+ listTodos(sessionId: string): Promise<Result<OpenCodeTodo[]>>;
296
+ /** File yang berubah di worktree project (git status versi opencode). */
297
+ fileStatus(): Promise<Result<OpenCodeFileChange[]>>;
298
+ replyPermission(requestId: string, reply: OpenCodePermissionReply): Promise<Result<unknown>>;
299
+ /**
300
+ * `answers` = SATU array label per pertanyaan (urut sesuai `questions`);
301
+ * request pertanyaan tunggal berarti array berisi satu elemen.
302
+ */
303
+ replyQuestion(requestId: string, answers: string[][]): Promise<Result<unknown>>;
304
+ rejectQuestion(requestId: string): Promise<Result<unknown>>;
305
+ abortSession(sessionId: string): Promise<Result<unknown>>;
306
+ /** Subscribe event SSE; mengembalikan fungsi untuk berhenti subscribe. */
307
+ subscribeEvents(cb: (ev: OpenCodeEvent) => void): () => void;
308
+ health(): Promise<boolean>;
309
+ }
310
+
311
+ function errResult(msg: string): { ok: false; error: string } {
312
+ return { ok: false, error: msg };
313
+ }
314
+
315
+ /**
316
+ * Error reply permission/question saat opencode membalas 404
317
+ * (`QuestionNotFoundError` / `PermissionNotFoundError`): request sudah
318
+ * tidak ditunggu lagi — dijawab dari klien lain, dibatalkan karena turn
319
+ * di-abort, atau server opencode di-restart. Pemanggil memperlakukannya
320
+ * sebagai prompt kedaluwarsa (bukan kegagalan yang bisa dicoba ulang).
321
+ */
322
+ export const OC_REQUEST_GONE = "OC_REQUEST_GONE";
323
+
324
+ async function requestJson(
325
+ baseUrl: string,
326
+ method: string,
327
+ path: string,
328
+ body?: unknown,
329
+ signal?: AbortSignal,
330
+ ): Promise<{ status: number; json: unknown }> {
331
+ const res = await fetch(`${baseUrl}${path}`, {
332
+ method,
333
+ headers: body !== undefined ? { "content-type": "application/json" } : undefined,
334
+ body: body !== undefined ? JSON.stringify(body) : undefined,
335
+ signal,
336
+ });
337
+ let json: unknown = null;
338
+ try {
339
+ json = await res.json();
340
+ } catch {
341
+ /* body kosong / bukan JSON */
342
+ }
343
+ return { status: res.status, json };
344
+ }
345
+
346
+ export function createOpenCodeClient(baseUrl: string): OpenCodeClient {
347
+ async function health(): Promise<boolean> {
348
+ try {
349
+ const res = await fetch(`${baseUrl}/global/health`, { signal: AbortSignal.timeout(3000) });
350
+ return res.ok;
351
+ } catch {
352
+ return false;
353
+ }
354
+ }
355
+
356
+ async function getSession(sessionId: string): Promise<SimpleResult> {
357
+ try {
358
+ const { status } = await requestJson(
359
+ baseUrl,
360
+ "GET",
361
+ `/session/${sessionId}`,
362
+ undefined,
363
+ AbortSignal.timeout(5000),
364
+ );
365
+ return status === 200 ? { ok: true } : errResult(`OC_SESSION_NOT_FOUND(${status})`);
366
+ } catch (e) {
367
+ return errResult(`OC_GET_SESSION_FAILED: ${(e as Error).message}`);
368
+ }
369
+ }
370
+
371
+ async function createSession(
372
+ opts: { title?: string } = {},
373
+ ): Promise<Result<OpenCodeSessionInfo>> {
374
+ try {
375
+ // Tanpa `title`: opencode memberi placeholder `New session - <ISO>` dan
376
+ // HANYA untuk judul berpola itu ia meng-generate judul otomatis
377
+ // (agent `title`) setelah prompt pertama. Judul eksplisit mematikannya.
378
+ const { status, json } = await requestJson(
379
+ baseUrl,
380
+ "POST",
381
+ "/session",
382
+ opts.title === undefined ? {} : { title: opts.title },
383
+ );
384
+ if (status !== 200) return errResult(`OC_CREATE_SESSION_FAILED(${status})`);
385
+ const info = json as OpenCodeSessionInfo;
386
+ if (typeof info?.id !== "string") return errResult("OC_CREATE_SESSION_FAILED");
387
+ return { ok: true, data: info };
388
+ } catch (e) {
389
+ return errResult(`OC_CREATE_SESSION_FAILED: ${(e as Error).message}`);
390
+ }
391
+ }
392
+
393
+ async function sendMessage(
394
+ sessionId: string,
395
+ text: string,
396
+ signal?: AbortSignal,
397
+ ): Promise<Result<OpenCodeMessageResult>> {
398
+ try {
399
+ const { status, json } = await requestJson(
400
+ baseUrl,
401
+ "POST",
402
+ `/session/${sessionId}/message`,
403
+ { parts: [{ type: "text", text }] },
404
+ signal,
405
+ );
406
+ if (status !== 200) return errResult(`OC_SEND_MESSAGE_FAILED(${status})`);
407
+ const body = json as OpenCodeMessageResult | null;
408
+ if (!body || typeof body.info?.id !== "string" || !Array.isArray(body.parts)) {
409
+ return errResult("OC_SEND_MESSAGE_FAILED: balasan tidak valid");
410
+ }
411
+ return { ok: true, data: body };
412
+ } catch (e) {
413
+ const err = e as Error & { name?: string };
414
+ const name = err.name ?? "";
415
+ const msg = err.message ?? "";
416
+ if (name === "TimeoutError" || name === "AbortError" || /timed out|aborted/i.test(msg)) {
417
+ return errResult("OC_SEND_MESSAGE_TIMEOUT");
418
+ }
419
+ return errResult(`OC_SEND_MESSAGE_FAILED: ${msg}`);
420
+ }
421
+ }
422
+
423
+ async function promptAsync(
424
+ sessionId: string,
425
+ text: string,
426
+ model?: SessionModel | null,
427
+ files: OpenCodeFileRef[] = [],
428
+ agent?: string | null,
429
+ system?: string | null,
430
+ ): Promise<Result<null>> {
431
+ try {
432
+ // Parts prompt: teks bebas + satu part `file` per referensi (teks/gambar).
433
+ const parts: Record<string, unknown>[] = [{ type: "text", text }];
434
+ for (const f of files) {
435
+ parts.push({ type: "file", mime: f.mime, filename: f.filename, url: f.url });
436
+ }
437
+ const body: Record<string, unknown> = { parts };
438
+ // Skema prompt_async menerima `model: { providerID, modelID }` opsional;
439
+ // tanpa field ini opencode memakai model default-nya.
440
+ if (model) body.model = { providerID: model.providerID, modelID: model.modelID };
441
+ // `agent` opsional (mis. build/plan atau agent kustom user); tanpa
442
+ // field ini opencode memakai agent default-nya.
443
+ if (agent) body.agent = agent;
444
+ // `system` opsional: custom instruction Project, ditambahkan opencode
445
+ // ke system prompt untuk turn ini (skema sama dengan POST /message).
446
+ if (system) body.system = system;
447
+ const { status } = await requestJson(
448
+ baseUrl,
449
+ "POST",
450
+ `/session/${sessionId}/prompt_async`,
451
+ body,
452
+ // Hanya menunggu penerimaan prompt (204), bukan seluruh turn.
453
+ AbortSignal.timeout(30_000),
454
+ );
455
+ // 204 = prompt diterima; opencode juga membalas 200 pada versi tertentu.
456
+ return status === 204 || status === 200
457
+ ? { ok: true, data: null }
458
+ : errResult(`OC_PROMPT_ASYNC_FAILED(${status})`);
459
+ } catch (e) {
460
+ return errResult(`OC_PROMPT_ASYNC_FAILED: ${(e as Error).message}`);
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Cari file di project (`GET /find/file?query=`) — dipakai autocomplete
466
+ * referensi `@file` di composer. Mengembalikan path relatif project.
467
+ */
468
+ async function findFiles(query: string): Promise<Result<string[]>> {
469
+ try {
470
+ const { status, json } = await requestJson(
471
+ baseUrl,
472
+ "GET",
473
+ `/find/file?query=${encodeURIComponent(query)}`,
474
+ undefined,
475
+ AbortSignal.timeout(5000),
476
+ );
477
+ if (status !== 200) return errResult(`OC_FIND_FILES_FAILED(${status})`);
478
+ const raw = Array.isArray(json) ? json : [];
479
+ const out: string[] = [];
480
+ for (const item of raw) {
481
+ if (typeof item === "string" && item.length > 0) out.push(item);
482
+ }
483
+ return { ok: true, data: out };
484
+ } catch (e) {
485
+ return errResult(`OC_FIND_FILES_FAILED: ${(e as Error).message}`);
486
+ }
487
+ }
488
+
489
+ async function listTodos(sessionId: string): Promise<Result<OpenCodeTodo[]>> {
490
+ try {
491
+ const { status, json } = await requestJson(
492
+ baseUrl,
493
+ "GET",
494
+ `/session/${sessionId}/todo`,
495
+ undefined,
496
+ AbortSignal.timeout(5000),
497
+ );
498
+ if (status !== 200) return errResult(`OC_TODO_FAILED(${status})`);
499
+ const out: OpenCodeTodo[] = [];
500
+ for (const item of Array.isArray(json) ? json : []) {
501
+ if (typeof item !== "object" || item === null) continue;
502
+ const t = item as { content?: unknown; status?: unknown; priority?: unknown };
503
+ if (typeof t.content !== "string" || t.content === "") continue;
504
+ out.push({
505
+ content: t.content,
506
+ status: typeof t.status === "string" ? t.status : "pending",
507
+ priority: typeof t.priority === "string" ? t.priority : "medium",
508
+ });
509
+ }
510
+ return { ok: true, data: out };
511
+ } catch (e) {
512
+ return errResult(`OC_TODO_FAILED: ${(e as Error).message}`);
513
+ }
514
+ }
515
+
516
+ async function fileStatus(): Promise<Result<OpenCodeFileChange[]>> {
517
+ try {
518
+ const { status, json } = await requestJson(
519
+ baseUrl,
520
+ "GET",
521
+ "/file/status",
522
+ undefined,
523
+ AbortSignal.timeout(5000),
524
+ );
525
+ if (status !== 200) return errResult(`OC_FILE_STATUS_FAILED(${status})`);
526
+ const out: OpenCodeFileChange[] = [];
527
+ for (const item of Array.isArray(json) ? json : []) {
528
+ if (typeof item !== "object" || item === null) continue;
529
+ const f = item as {
530
+ path?: unknown;
531
+ added?: unknown;
532
+ removed?: unknown;
533
+ status?: unknown;
534
+ };
535
+ if (typeof f.path !== "string" || f.path === "") continue;
536
+ const kind =
537
+ f.status === "added" || f.status === "deleted" || f.status === "modified"
538
+ ? f.status
539
+ : "modified";
540
+ out.push({
541
+ path: f.path,
542
+ added: typeof f.added === "number" ? f.added : 0,
543
+ removed: typeof f.removed === "number" ? f.removed : 0,
544
+ status: kind,
545
+ });
546
+ }
547
+ return { ok: true, data: out };
548
+ } catch (e) {
549
+ return errResult(`OC_FILE_STATUS_FAILED: ${(e as Error).message}`);
550
+ }
551
+ }
552
+
553
+ async function listModels(): Promise<Result<ModelOption[]>> {
554
+ try {
555
+ const { status, json } = await requestJson(baseUrl, "GET", "/config/providers");
556
+ if (status !== 200) return errResult(`OC_LIST_MODELS_FAILED(${status})`);
557
+ return { ok: true, data: flattenProviders(json) };
558
+ } catch (e) {
559
+ return errResult(`OC_LIST_MODELS_FAILED: ${(e as Error).message}`);
560
+ }
561
+ }
562
+
563
+ async function listAgents(): Promise<Result<AgentOption[]>> {
564
+ try {
565
+ const { status, json } = await requestJson(baseUrl, "GET", "/agent");
566
+ if (status !== 200) return errResult(`OC_LIST_AGENTS_FAILED(${status})`);
567
+ return { ok: true, data: flattenAgents(json) };
568
+ } catch (e) {
569
+ return errResult(`OC_LIST_AGENTS_FAILED: ${(e as Error).message}`);
570
+ }
571
+ }
572
+
573
+ async function listMcp(): Promise<Result<McpServerInfo[]>> {
574
+ try {
575
+ const { status, json } = await requestJson(
576
+ baseUrl,
577
+ "GET",
578
+ "/mcp",
579
+ undefined,
580
+ AbortSignal.timeout(10_000),
581
+ );
582
+ if (status !== 200) return errResult(`OC_LIST_MCP_FAILED(${status})`);
583
+ return { ok: true, data: flattenMcp(json) };
584
+ } catch (e) {
585
+ return errResult(`OC_LIST_MCP_FAILED: ${(e as Error).message}`);
586
+ }
587
+ }
588
+
589
+ async function listSkills(projectPath?: string): Promise<Result<SkillInfo[]>> {
590
+ try {
591
+ const { status, json } = await requestJson(
592
+ baseUrl,
593
+ "GET",
594
+ "/skill",
595
+ undefined,
596
+ AbortSignal.timeout(10_000),
597
+ );
598
+ if (status !== 200) return errResult(`OC_LIST_SKILLS_FAILED(${status})`);
599
+ return { ok: true, data: flattenSkills(json, projectPath) };
600
+ } catch (e) {
601
+ return errResult(`OC_LIST_SKILLS_FAILED: ${(e as Error).message}`);
602
+ }
603
+ }
604
+
605
+ async function disposeInstance(): Promise<SimpleResult> {
606
+ try {
607
+ const { status } = await requestJson(
608
+ baseUrl,
609
+ "POST",
610
+ "/instance/dispose",
611
+ undefined,
612
+ AbortSignal.timeout(10_000),
613
+ );
614
+ return status === 200 ? { ok: true } : { ok: false, error: `OC_DISPOSE_FAILED(${status})` };
615
+ } catch (e) {
616
+ return { ok: false, error: `OC_DISPOSE_FAILED: ${(e as Error).message}` };
617
+ }
618
+ }
619
+
620
+ async function deleteSession(sessionId: string): Promise<SimpleResult> {
621
+ try {
622
+ const { status } = await requestJson(
623
+ baseUrl,
624
+ "DELETE",
625
+ `/session/${sessionId}`,
626
+ undefined,
627
+ AbortSignal.timeout(10_000),
628
+ );
629
+ // 200 = terhapus. 404 dianggap sukses: target sudah tidak ada (idemoten
630
+ // terhadap storage opencode yang mungkin sudah dibersihkan manual).
631
+ return status === 200 || status === 404
632
+ ? { ok: true }
633
+ : errResult(`OC_DELETE_SESSION_FAILED(${status})`);
634
+ } catch (e) {
635
+ return errResult(`OC_DELETE_SESSION_FAILED: ${(e as Error).message}`);
636
+ }
637
+ }
638
+
639
+ async function replyPermission(
640
+ requestId: string,
641
+ reply: OpenCodePermissionReply,
642
+ ): Promise<Result<unknown>> {
643
+ try {
644
+ const { status } = await requestJson(baseUrl, "POST", `/permission/${requestId}/reply`, {
645
+ reply,
646
+ });
647
+ if (status === 200) return { ok: true, data: null };
648
+ return errResult(status === 404 ? OC_REQUEST_GONE : `OC_PERMISSION_REPLY_FAILED(${status})`);
649
+ } catch (e) {
650
+ return errResult(`OC_PERMISSION_REPLY_FAILED: ${(e as Error).message}`);
651
+ }
652
+ }
653
+
654
+ async function replyQuestion(requestId: string, answers: string[][]): Promise<Result<unknown>> {
655
+ try {
656
+ // Skema Question.Reply: `{ answers: Answer[] }` — SATU entri per
657
+ // pertanyaan, masing-masing berupa array label terpilih. Pemanggil
658
+ // sudah mengirim satu array per pertanyaan, jadi body diteruskan
659
+ // apa adanya.
660
+ const { status } = await requestJson(baseUrl, "POST", `/question/${requestId}/reply`, {
661
+ answers,
662
+ });
663
+ if (status === 200) return { ok: true, data: null };
664
+ return errResult(status === 404 ? OC_REQUEST_GONE : `OC_QUESTION_REPLY_FAILED(${status})`);
665
+ } catch (e) {
666
+ return errResult(`OC_QUESTION_REPLY_FAILED: ${(e as Error).message}`);
667
+ }
668
+ }
669
+
670
+ async function rejectQuestion(requestId: string): Promise<Result<unknown>> {
671
+ try {
672
+ const { status } = await requestJson(baseUrl, "POST", `/question/${requestId}/reject`, {});
673
+ if (status === 200) return { ok: true, data: null };
674
+ return errResult(status === 404 ? OC_REQUEST_GONE : `OC_QUESTION_REJECT_FAILED(${status})`);
675
+ } catch (e) {
676
+ return errResult(`OC_QUESTION_REJECT_FAILED: ${(e as Error).message}`);
677
+ }
678
+ }
679
+
680
+ async function abortSession(sessionId: string): Promise<Result<unknown>> {
681
+ try {
682
+ const { status } = await requestJson(baseUrl, "POST", `/session/${sessionId}/abort`, {});
683
+ return status === 200 ? { ok: true, data: null } : errResult(`OC_ABORT_FAILED(${status})`);
684
+ } catch (e) {
685
+ return errResult(`OC_ABORT_FAILED: ${(e as Error).message}`);
686
+ }
687
+ }
688
+
689
+ function subscribeEvents(cb: (ev: OpenCodeEvent) => void): () => void {
690
+ return subscribeOpenCodeEvents(baseUrl, cb);
691
+ }
692
+
693
+ return {
694
+ createSession,
695
+ getSession,
696
+ listModels,
697
+ listAgents,
698
+ deleteSession,
699
+ sendMessage,
700
+ promptAsync,
701
+ listMcp,
702
+ listSkills,
703
+ disposeInstance,
704
+ findFiles,
705
+ listTodos,
706
+ fileStatus,
707
+ replyPermission,
708
+ replyQuestion,
709
+ rejectQuestion,
710
+ abortSession,
711
+ subscribeEvents,
712
+ health,
713
+ };
714
+ }