dodo-mcp 1.0.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 (447) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +425 -0
  3. package/dist/auth/adapter.js +43 -0
  4. package/dist/auth/adapter.js.map +1 -0
  5. package/dist/auth/clientDeletion.js +45 -0
  6. package/dist/auth/clientDeletion.js.map +1 -0
  7. package/dist/auth/clients.js +55 -0
  8. package/dist/auth/clients.js.map +1 -0
  9. package/dist/auth/constants.js +10 -0
  10. package/dist/auth/constants.js.map +1 -0
  11. package/dist/auth/interactions.js +184 -0
  12. package/dist/auth/interactions.js.map +1 -0
  13. package/dist/auth/keys.js +56 -0
  14. package/dist/auth/keys.js.map +1 -0
  15. package/dist/auth/provider.js +131 -0
  16. package/dist/auth/provider.js.map +1 -0
  17. package/dist/auth/verifier.js +74 -0
  18. package/dist/auth/verifier.js.map +1 -0
  19. package/dist/cli/kill.js +124 -0
  20. package/dist/cli/kill.js.map +1 -0
  21. package/dist/cli/main.js +1393 -0
  22. package/dist/cli/main.js.map +1 -0
  23. package/dist/cli/menu.js +127 -0
  24. package/dist/cli/menu.js.map +1 -0
  25. package/dist/cli/terminal.js +30 -0
  26. package/dist/cli/terminal.js.map +1 -0
  27. package/dist/config/globalConfig.js +144 -0
  28. package/dist/config/globalConfig.js.map +1 -0
  29. package/dist/config/limits.js +42 -0
  30. package/dist/config/limits.js.map +1 -0
  31. package/dist/config/paths.js +94 -0
  32. package/dist/config/paths.js.map +1 -0
  33. package/dist/config/projectConfig.js +63 -0
  34. package/dist/config/projectConfig.js.map +1 -0
  35. package/dist/config/stateImport.js +306 -0
  36. package/dist/config/stateImport.js.map +1 -0
  37. package/dist/config/tunnelConfig.js +23 -0
  38. package/dist/config/tunnelConfig.js.map +1 -0
  39. package/dist/errors.js +82 -0
  40. package/dist/errors.js.map +1 -0
  41. package/dist/evaluation/contracts.js +86 -0
  42. package/dist/evaluation/contracts.js.map +1 -0
  43. package/dist/evaluation/dodoBench.js +69 -0
  44. package/dist/evaluation/dodoBench.js.map +1 -0
  45. package/dist/index.js +26 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/ipc/authentication.js +117 -0
  48. package/dist/ipc/authentication.js.map +1 -0
  49. package/dist/ipc/client.js +86 -0
  50. package/dist/ipc/client.js.map +1 -0
  51. package/dist/ipc/installationClient.js +83 -0
  52. package/dist/ipc/installationClient.js.map +1 -0
  53. package/dist/ipc/ownerControl.js +46 -0
  54. package/dist/ipc/ownerControl.js.map +1 -0
  55. package/dist/ipc/protocol.js +2 -0
  56. package/dist/ipc/protocol.js.map +1 -0
  57. package/dist/ipc/server.js +108 -0
  58. package/dist/ipc/server.js.map +1 -0
  59. package/dist/platform/execResolve.js +64 -0
  60. package/dist/platform/execResolve.js.map +1 -0
  61. package/dist/platform/fileIdentity.js +26 -0
  62. package/dist/platform/fileIdentity.js.map +1 -0
  63. package/dist/platform/fsRetry.js +22 -0
  64. package/dist/platform/fsRetry.js.map +1 -0
  65. package/dist/platform/pathPolicy.js +29 -0
  66. package/dist/platform/pathPolicy.js.map +1 -0
  67. package/dist/platform/privateFs.js +116 -0
  68. package/dist/platform/privateFs.js.map +1 -0
  69. package/dist/platform/processTree.js +31 -0
  70. package/dist/platform/processTree.js.map +1 -0
  71. package/dist/platform/shell.js +74 -0
  72. package/dist/platform/shell.js.map +1 -0
  73. package/dist/platform/speech.js +94 -0
  74. package/dist/platform/speech.js.map +1 -0
  75. package/dist/platform/system.js +28 -0
  76. package/dist/platform/system.js.map +1 -0
  77. package/dist/platform/windowsSandbox.js +129 -0
  78. package/dist/platform/windowsSandbox.js.map +1 -0
  79. package/dist/projects/federation.js +312 -0
  80. package/dist/projects/federation.js.map +1 -0
  81. package/dist/projects/registry.js +291 -0
  82. package/dist/projects/registry.js.map +1 -0
  83. package/dist/security/accessMode.js +19 -0
  84. package/dist/security/accessMode.js.map +1 -0
  85. package/dist/security/approvals.js +56 -0
  86. package/dist/security/approvals.js.map +1 -0
  87. package/dist/security/env.js +57 -0
  88. package/dist/security/env.js.map +1 -0
  89. package/dist/security/mutationQueue.js +78 -0
  90. package/dist/security/mutationQueue.js.map +1 -0
  91. package/dist/security/policy.js +22 -0
  92. package/dist/security/policy.js.map +1 -0
  93. package/dist/security/projectAuthority.js +22 -0
  94. package/dist/security/projectAuthority.js.map +1 -0
  95. package/dist/security/redact.js +30 -0
  96. package/dist/security/redact.js.map +1 -0
  97. package/dist/server/aiAdmin.js +201 -0
  98. package/dist/server/aiAdmin.js.map +1 -0
  99. package/dist/server/appServer.js +461 -0
  100. package/dist/server/appServer.js.map +1 -0
  101. package/dist/server/bootstrap.js +185 -0
  102. package/dist/server/bootstrap.js.map +1 -0
  103. package/dist/server/configUi/app.css +347 -0
  104. package/dist/server/configUi/app.js +958 -0
  105. package/dist/server/configUi/index.html +379 -0
  106. package/dist/server/configUi/ui/alerts.js +92 -0
  107. package/dist/server/configUi/ui/dom.js +19 -0
  108. package/dist/server/configUi/ui/tooltips.js +115 -0
  109. package/dist/server/configUi/vendor/VERSION.txt +1 -0
  110. package/dist/server/configUi/vendor/sweetalert2.min.css +1 -0
  111. package/dist/server/configUi/vendor/sweetalert2.min.js +5 -0
  112. package/dist/server/configUi/workbench.css +62 -0
  113. package/dist/server/configUi/workbench.js +394 -0
  114. package/dist/server/installationRuntime.js +155 -0
  115. package/dist/server/installationRuntime.js.map +1 -0
  116. package/dist/server/instructions.js +17 -0
  117. package/dist/server/instructions.js.map +1 -0
  118. package/dist/server/ipcDispatch.js +225 -0
  119. package/dist/server/ipcDispatch.js.map +1 -0
  120. package/dist/server/localConfig.js +561 -0
  121. package/dist/server/localConfig.js.map +1 -0
  122. package/dist/server/optionalServices.js +30 -0
  123. package/dist/server/optionalServices.js.map +1 -0
  124. package/dist/server/projectLease.js +46 -0
  125. package/dist/server/projectLease.js.map +1 -0
  126. package/dist/server/stdioServer.js +75 -0
  127. package/dist/server/stdioServer.js.map +1 -0
  128. package/dist/server/version.js +13 -0
  129. package/dist/server/version.js.map +1 -0
  130. package/dist/server/workspaceHost.js +194 -0
  131. package/dist/server/workspaceHost.js.map +1 -0
  132. package/dist/services/agent/agentService.js +598 -0
  133. package/dist/services/agent/agentService.js.map +1 -0
  134. package/dist/services/agent/contracts.js +102 -0
  135. package/dist/services/agent/contracts.js.map +1 -0
  136. package/dist/services/ai/adapters.js +287 -0
  137. package/dist/services/ai/adapters.js.map +1 -0
  138. package/dist/services/ai/contracts.js +39 -0
  139. package/dist/services/ai/contracts.js.map +1 -0
  140. package/dist/services/ai/credentials.js +77 -0
  141. package/dist/services/ai/credentials.js.map +1 -0
  142. package/dist/services/ai/network.js +113 -0
  143. package/dist/services/ai/network.js.map +1 -0
  144. package/dist/services/ai/settings.js +178 -0
  145. package/dist/services/ai/settings.js.map +1 -0
  146. package/dist/services/ai/subagents.js +468 -0
  147. package/dist/services/ai/subagents.js.map +1 -0
  148. package/dist/services/assistance/analysis.js +352 -0
  149. package/dist/services/assistance/analysis.js.map +1 -0
  150. package/dist/services/assistance/contracts.js +54 -0
  151. package/dist/services/assistance/contracts.js.map +1 -0
  152. package/dist/services/assistance/verification.js +274 -0
  153. package/dist/services/assistance/verification.js.map +1 -0
  154. package/dist/services/brain/brainParser.js +97 -0
  155. package/dist/services/brain/brainParser.js.map +1 -0
  156. package/dist/services/brain/brainService.js +733 -0
  157. package/dist/services/brain/brainService.js.map +1 -0
  158. package/dist/services/brain/brainWorker.js +212 -0
  159. package/dist/services/brain/brainWorker.js.map +1 -0
  160. package/dist/services/brain/contracts.js +154 -0
  161. package/dist/services/brain/contracts.js.map +1 -0
  162. package/dist/services/brain/protocol.js +2 -0
  163. package/dist/services/brain/protocol.js.map +1 -0
  164. package/dist/services/changes/applier.js +544 -0
  165. package/dist/services/changes/applier.js.map +1 -0
  166. package/dist/services/changes/contentBudget.js +11 -0
  167. package/dist/services/changes/contentBudget.js.map +1 -0
  168. package/dist/services/changes/planner.js +284 -0
  169. package/dist/services/changes/planner.js.map +1 -0
  170. package/dist/services/changes/types.js +3 -0
  171. package/dist/services/changes/types.js.map +1 -0
  172. package/dist/services/context/contextEngine.js +746 -0
  173. package/dist/services/context/contextEngine.js.map +1 -0
  174. package/dist/services/context/contracts.js +88 -0
  175. package/dist/services/context/contracts.js.map +1 -0
  176. package/dist/services/desktop/desktopPolicy.js +76 -0
  177. package/dist/services/desktop/desktopPolicy.js.map +1 -0
  178. package/dist/services/desktop/desktopService.js +130 -0
  179. package/dist/services/desktop/desktopService.js.map +1 -0
  180. package/dist/services/desktop/nativeBackend.js +174 -0
  181. package/dist/services/desktop/nativeBackend.js.map +1 -0
  182. package/dist/services/desktop/protocol.js +32 -0
  183. package/dist/services/desktop/protocol.js.map +1 -0
  184. package/dist/services/files/readService.js +136 -0
  185. package/dist/services/files/readService.js.map +1 -0
  186. package/dist/services/git/gitService.js +369 -0
  187. package/dist/services/git/gitService.js.map +1 -0
  188. package/dist/services/intelligence/intelService.js +136 -0
  189. package/dist/services/intelligence/intelService.js.map +1 -0
  190. package/dist/services/intelligence/protocol.js +2 -0
  191. package/dist/services/intelligence/protocol.js.map +1 -0
  192. package/dist/services/intelligence/tsWorker.js +419 -0
  193. package/dist/services/intelligence/tsWorker.js.map +1 -0
  194. package/dist/services/jobs/commandInput.js +34 -0
  195. package/dist/services/jobs/commandInput.js.map +1 -0
  196. package/dist/services/jobs/jobManager.js +398 -0
  197. package/dist/services/jobs/jobManager.js.map +1 -0
  198. package/dist/services/jobs/sandbox.js +156 -0
  199. package/dist/services/jobs/sandbox.js.map +1 -0
  200. package/dist/services/jobs/sandboxWiring.js +37 -0
  201. package/dist/services/jobs/sandboxWiring.js.map +1 -0
  202. package/dist/services/jobs/spool.js +112 -0
  203. package/dist/services/jobs/spool.js.map +1 -0
  204. package/dist/services/jobs/windowsSandboxChild.js +24 -0
  205. package/dist/services/jobs/windowsSandboxChild.js.map +1 -0
  206. package/dist/services/jobs/windowsSandboxWorker.js +155 -0
  207. package/dist/services/jobs/windowsSandboxWorker.js.map +1 -0
  208. package/dist/services/lsp/documentUri.js +16 -0
  209. package/dist/services/lsp/documentUri.js.map +1 -0
  210. package/dist/services/lsp/jsonrpc.js +382 -0
  211. package/dist/services/lsp/jsonrpc.js.map +1 -0
  212. package/dist/services/lsp/lspService.js +1051 -0
  213. package/dist/services/lsp/lspService.js.map +1 -0
  214. package/dist/services/memory/contracts.js +94 -0
  215. package/dist/services/memory/contracts.js.map +1 -0
  216. package/dist/services/memory/memoryService.js +683 -0
  217. package/dist/services/memory/memoryService.js.map +1 -0
  218. package/dist/services/multimodal/browserService.js +407 -0
  219. package/dist/services/multimodal/browserService.js.map +1 -0
  220. package/dist/services/multimodal/byteRange.js +31 -0
  221. package/dist/services/multimodal/byteRange.js.map +1 -0
  222. package/dist/services/multimodal/contracts.js +59 -0
  223. package/dist/services/multimodal/contracts.js.map +1 -0
  224. package/dist/services/multimodal/gameService.js +95 -0
  225. package/dist/services/multimodal/gameService.js.map +1 -0
  226. package/dist/services/multimodal/images.js +107 -0
  227. package/dist/services/multimodal/images.js.map +1 -0
  228. package/dist/services/multimodal/mediaService.js +279 -0
  229. package/dist/services/multimodal/mediaService.js.map +1 -0
  230. package/dist/services/multimodal/mediaWorker.js +143 -0
  231. package/dist/services/multimodal/mediaWorker.js.map +1 -0
  232. package/dist/services/multimodal/multimodalService.js +51 -0
  233. package/dist/services/multimodal/multimodalService.js.map +1 -0
  234. package/dist/services/multimodal/network.js +69 -0
  235. package/dist/services/multimodal/network.js.map +1 -0
  236. package/dist/services/multimodal/operations.js +40 -0
  237. package/dist/services/multimodal/operations.js.map +1 -0
  238. package/dist/services/multimodal/storage.js +160 -0
  239. package/dist/services/multimodal/storage.js.map +1 -0
  240. package/dist/services/multimodal/workflowService.js +179 -0
  241. package/dist/services/multimodal/workflowService.js.map +1 -0
  242. package/dist/services/overview.js +284 -0
  243. package/dist/services/overview.js.map +1 -0
  244. package/dist/services/resources/casStore.js +304 -0
  245. package/dist/services/resources/casStore.js.map +1 -0
  246. package/dist/services/resources/contracts.js +75 -0
  247. package/dist/services/resources/contracts.js.map +1 -0
  248. package/dist/services/resources/mime.js +77 -0
  249. package/dist/services/resources/mime.js.map +1 -0
  250. package/dist/services/resources/providerRegistry.js +22 -0
  251. package/dist/services/resources/providerRegistry.js.map +1 -0
  252. package/dist/services/resources/providers.js +120 -0
  253. package/dist/services/resources/providers.js.map +1 -0
  254. package/dist/services/resources/resourceService.js +282 -0
  255. package/dist/services/resources/resourceService.js.map +1 -0
  256. package/dist/services/runtime/contracts.js +65 -0
  257. package/dist/services/runtime/contracts.js.map +1 -0
  258. package/dist/services/runtime/runtimeService.js +364 -0
  259. package/dist/services/runtime/runtimeService.js.map +1 -0
  260. package/dist/services/schedules/scheduleService.js +198 -0
  261. package/dist/services/schedules/scheduleService.js.map +1 -0
  262. package/dist/services/search/searchService.js +434 -0
  263. package/dist/services/search/searchService.js.map +1 -0
  264. package/dist/services/search/searchWorker.js +58 -0
  265. package/dist/services/search/searchWorker.js.map +1 -0
  266. package/dist/setup/componentProbes.js +259 -0
  267. package/dist/setup/componentProbes.js.map +1 -0
  268. package/dist/setup/download.js +139 -0
  269. package/dist/setup/download.js.map +1 -0
  270. package/dist/setup/managedTools.js +118 -0
  271. package/dist/setup/managedTools.js.map +1 -0
  272. package/dist/setup/sandboxProbe.js +172 -0
  273. package/dist/setup/sandboxProbe.js.map +1 -0
  274. package/dist/setup/setup.js +522 -0
  275. package/dist/setup/setup.js.map +1 -0
  276. package/dist/setup/windowsChromiumAcl.js +56 -0
  277. package/dist/setup/windowsChromiumAcl.js.map +1 -0
  278. package/dist/setup/windowsSandboxInstall.js +94 -0
  279. package/dist/setup/windowsSandboxInstall.js.map +1 -0
  280. package/dist/setup/windowsSpeechInstall.js +86 -0
  281. package/dist/setup/windowsSpeechInstall.js.map +1 -0
  282. package/dist/store/db.js +711 -0
  283. package/dist/store/db.js.map +1 -0
  284. package/dist/store/scopeEncoding.js +15 -0
  285. package/dist/store/scopeEncoding.js.map +1 -0
  286. package/dist/store/store.js +467 -0
  287. package/dist/store/store.js.map +1 -0
  288. package/dist/tools/agentRuntimeTools.js +216 -0
  289. package/dist/tools/agentRuntimeTools.js.map +1 -0
  290. package/dist/tools/agentTools.js +317 -0
  291. package/dist/tools/agentTools.js.map +1 -0
  292. package/dist/tools/assistanceTools.js +126 -0
  293. package/dist/tools/assistanceTools.js.map +1 -0
  294. package/dist/tools/brainTools.js +130 -0
  295. package/dist/tools/brainTools.js.map +1 -0
  296. package/dist/tools/catalog.js +30 -0
  297. package/dist/tools/catalog.js.map +1 -0
  298. package/dist/tools/changeTools.js +188 -0
  299. package/dist/tools/changeTools.js.map +1 -0
  300. package/dist/tools/context.js +243 -0
  301. package/dist/tools/context.js.map +1 -0
  302. package/dist/tools/contextTools.js +57 -0
  303. package/dist/tools/contextTools.js.map +1 -0
  304. package/dist/tools/coreCatalog.js +82 -0
  305. package/dist/tools/coreCatalog.js.map +1 -0
  306. package/dist/tools/desktopTools.js +41 -0
  307. package/dist/tools/desktopTools.js.map +1 -0
  308. package/dist/tools/directTools.js +273 -0
  309. package/dist/tools/directTools.js.map +1 -0
  310. package/dist/tools/envelope.js +71 -0
  311. package/dist/tools/envelope.js.map +1 -0
  312. package/dist/tools/extraTools.js +298 -0
  313. package/dist/tools/extraTools.js.map +1 -0
  314. package/dist/tools/gitTools.js +93 -0
  315. package/dist/tools/gitTools.js.map +1 -0
  316. package/dist/tools/jobTools.js +454 -0
  317. package/dist/tools/jobTools.js.map +1 -0
  318. package/dist/tools/memoryTools.js +102 -0
  319. package/dist/tools/memoryTools.js.map +1 -0
  320. package/dist/tools/metaTools.js +153 -0
  321. package/dist/tools/metaTools.js.map +1 -0
  322. package/dist/tools/multimodalTools.js +262 -0
  323. package/dist/tools/multimodalTools.js.map +1 -0
  324. package/dist/tools/projectTools.js +261 -0
  325. package/dist/tools/projectTools.js.map +1 -0
  326. package/dist/tools/resourceTools.js +81 -0
  327. package/dist/tools/resourceTools.js.map +1 -0
  328. package/dist/tools/runtimeTools.js +137 -0
  329. package/dist/tools/runtimeTools.js.map +1 -0
  330. package/dist/tools/scheduleTools.js +10 -0
  331. package/dist/tools/scheduleTools.js.map +1 -0
  332. package/dist/tools/semanticTools.js +141 -0
  333. package/dist/tools/semanticTools.js.map +1 -0
  334. package/dist/tools/subagentTools.js +32 -0
  335. package/dist/tools/subagentTools.js.map +1 -0
  336. package/dist/tools/surface.js +644 -0
  337. package/dist/tools/surface.js.map +1 -0
  338. package/dist/tunnel/control.js +113 -0
  339. package/dist/tunnel/control.js.map +1 -0
  340. package/dist/tunnel/credentials.js +392 -0
  341. package/dist/tunnel/credentials.js.map +1 -0
  342. package/dist/tunnel/log.js +84 -0
  343. package/dist/tunnel/log.js.map +1 -0
  344. package/dist/tunnel/runtime.js +55 -0
  345. package/dist/tunnel/runtime.js.map +1 -0
  346. package/dist/tunnel/supervisor.js +280 -0
  347. package/dist/tunnel/supervisor.js.map +1 -0
  348. package/dist/util/bytes.js +53 -0
  349. package/dist/util/bytes.js.map +1 -0
  350. package/dist/util/hash.js +53 -0
  351. package/dist/util/hash.js.map +1 -0
  352. package/dist/workspace/fs.js +305 -0
  353. package/dist/workspace/fs.js.map +1 -0
  354. package/dist/workspace/identity.js +17 -0
  355. package/dist/workspace/identity.js.map +1 -0
  356. package/dist/workspace/ignores.js +173 -0
  357. package/dist/workspace/ignores.js.map +1 -0
  358. package/dist/workspace/root.js +59 -0
  359. package/dist/workspace/root.js.map +1 -0
  360. package/docs/AGENT_RUNTIME.md +154 -0
  361. package/docs/AI_PROVIDERS.md +110 -0
  362. package/docs/ARCHITECTURE.md +331 -0
  363. package/docs/ASSISTANCE.md +31 -0
  364. package/docs/AUTH.md +59 -0
  365. package/docs/BRAIN.md +92 -0
  366. package/docs/COMPATIBILITY.md +92 -0
  367. package/docs/CONFIRMATION.md +13 -0
  368. package/docs/CONTEXT.md +87 -0
  369. package/docs/DESKTOP.md +35 -0
  370. package/docs/EVALUATION.md +86 -0
  371. package/docs/LARGE_INPUTS.md +21 -0
  372. package/docs/LSP.md +21 -0
  373. package/docs/MANUAL_ACCEPTANCE.md +283 -0
  374. package/docs/MEMORY.md +128 -0
  375. package/docs/MULTIMODAL.md +32 -0
  376. package/docs/PROJECTS.md +146 -0
  377. package/docs/RELEASE_1.0.0.md +111 -0
  378. package/docs/RELEASE_NOTES.md +92 -0
  379. package/docs/RESOURCES.md +83 -0
  380. package/docs/RUNTIME.md +125 -0
  381. package/docs/SCHEDULES.md +23 -0
  382. package/docs/SECURITY.md +250 -0
  383. package/docs/TEST_REPORT.md +312 -0
  384. package/docs/TUNNEL.md +86 -0
  385. package/docs/WEB_CLIENTS.md +43 -0
  386. package/docs/WINDOWS.md +79 -0
  387. package/docs/adr/001-one-process-one-workspace.md +22 -0
  388. package/docs/adr/002-external-tunnel.md +19 -0
  389. package/docs/adr/003-sdk-v2-legacy-stateless.md +20 -0
  390. package/docs/adr/004-embedded-oauth-provider.md +19 -0
  391. package/docs/adr/005-static-client-baseline.md +19 -0
  392. package/docs/adr/006-guarded-fs-explicit-exec.md +16 -0
  393. package/docs/adr/007-preview-hash-journal.md +18 -0
  394. package/docs/adr/008-ts-js-semantics-first.md +17 -0
  395. package/docs/adr/009-jobs-polling.md +18 -0
  396. package/docs/adr/010-sqlite-better-sqlite3.md +18 -0
  397. package/docs/adr/011-env-example-secure-default.md +19 -0
  398. package/docs/adr/012-workspace-scope-grant.md +24 -0
  399. package/docs/adr/013-loopback-proxy-trust.md +18 -0
  400. package/docs/adr/014-direct-coding-tier.md +48 -0
  401. package/docs/adr/015-coding-agent-parity.md +56 -0
  402. package/docs/adr/016-oidc-openid-consent.md +15 -0
  403. package/docs/adr/017-installation-identity-local-config.md +44 -0
  404. package/docs/adr/018-access-scope-encoding.md +11 -0
  405. package/docs/adr/019-runtime-workspace-switch.md +59 -0
  406. package/docs/adr/020-desktop-access.md +50 -0
  407. package/docs/adr/021-persistent-desktop-consent.md +39 -0
  408. package/docs/adr/022-owner-kill-preserves-login.md +30 -0
  409. package/docs/adr/023-large-coding-inputs.md +49 -0
  410. package/docs/adr/024-usage-rounds-and-schedules.md +72 -0
  411. package/docs/adr/025-owner-chat-whitelist.md +53 -0
  412. package/docs/adr/026-remove-conversation-confirmation.md +35 -0
  413. package/docs/adr/027-workspace-client-list.md +20 -0
  414. package/docs/adr/028-owner-client-deletion.md +25 -0
  415. package/docs/adr/029-compact-tool-surface.md +121 -0
  416. package/docs/adr/030-windows-native-candidate.md +71 -0
  417. package/docs/adr/031-owner-selected-tunnel-supervision.md +13 -0
  418. package/docs/adr/032-owner-project-registry.md +43 -0
  419. package/docs/adr/033-read-only-project-federation.md +61 -0
  420. package/docs/adr/034-universal-resource-cas.md +60 -0
  421. package/docs/adr/035-project-brain-incremental-index.md +51 -0
  422. package/docs/adr/036-context-engine-evidence.md +53 -0
  423. package/docs/adr/037-owner-reviewed-memory.md +46 -0
  424. package/docs/adr/038-runtime-intelligence.md +46 -0
  425. package/docs/adr/039-advanced-agent-runtime.md +57 -0
  426. package/docs/adr/040-dodobench-release-gate.md +43 -0
  427. package/docs/adr/041-local-macos-docker-linux-gates.md +32 -0
  428. package/docs/adr/042-project-directory-generation-identity.md +36 -0
  429. package/docs/adr/043-trusted-self-hosted-platform-gates.md +38 -0
  430. package/docs/adr/044-global-launcher-and-cli-menu.md +43 -0
  431. package/docs/adr/045-ai-providers-multiproject.md +56 -0
  432. package/docs/adr/046-personal-access-mode.md +32 -0
  433. package/docs/adr/README.md +64 -0
  434. package/examples/.dodo.json.example +20 -0
  435. package/examples/cloudflared.yml +23 -0
  436. package/examples/reverse-proxy-fixture.mjs +37 -0
  437. package/native/desktop-linux.py +387 -0
  438. package/native/desktop-windows.cs +180 -0
  439. package/native/desktop.swift +212 -0
  440. package/package.json +145 -0
  441. package/schemas/global-config.schema.json +497 -0
  442. package/schemas/project-config.schema.json +75 -0
  443. package/schemas/tools.compact.json +3236 -0
  444. package/schemas/tools.hybrid.json +8194 -0
  445. package/schemas/tools.json +34518 -0
  446. package/scripts/guard-publish.mjs +12 -0
  447. package/scripts/setup-multimodal.mjs +70 -0
@@ -0,0 +1,394 @@
1
+ /* Owner-only workbench + page router. Provider keys are submitted once and
2
+ * never persisted in browser storage. Every server-derived string is rendered
3
+ * with textContent (via DodoUI.el); confirmations/toasts use the vendored
4
+ * SweetAlert2 wrappers (DodoUI.alerts) with the <dialog> fallback — browser
5
+ * alert()/confirm() are never the primary UI. */
6
+ 'use strict';
7
+ (() => {
8
+ const $ = id => document.getElementById(id);
9
+ const UI = window.DodoUI || {};
10
+ const alerts = () => (UI.alerts && UI.alerts.available() ? UI.alerts : null);
11
+ const panel = $('workbench');
12
+ let state, selected, stream, currentView, followRun;
13
+ const el = UI.el || ((tag, text, className) => { const n = document.createElement(tag); if (text !== undefined) n.textContent = text; if (className) n.className = className; return n; });
14
+ const timeText = (value) => new Date(value ?? Date.now()).toLocaleTimeString('th-TH');
15
+ const help = (text, subject) => (UI.tooltips ? UI.tooltips.helpButton(text, subject) : el('span'));
16
+
17
+ /** Which legacy card containers belong to each nav view (IDs in index.html). */
18
+ const VIEW_PAGES = { projects: ['page-projects'], admin: ['page-admin'], settings: ['page-settings'] };
19
+ const PROJECT_SCOPED = new Set(['projects', 'chat', 'runs', 'knowledge', 'admin']);
20
+ const STATUS_TH = { running: 'กำลังทำงาน', completed: 'เสร็จแล้ว', failed: 'ล้มเหลว', canceled: 'ยกเลิกแล้ว', paused: 'พักไว้', queued: 'รอคิว', interrupted: 'ถูกขัดจังหวะ — ต้อง Resume', waiting_approval: 'รออนุมัติจากเจ้าของ', waiting_auth: 'รอยืนยันตัวตนกับ provider' };
21
+
22
+ const headers = (target = false) => {
23
+ const c = target ? selected : state?.controlContext;
24
+ return { Authorization: `Bearer ${sessionStorage.getItem('dodo-config-token') || ''}`, 'Content-Type':'application/json', ...(c ? { 'x-dodo-workspace':c.workspaceId, 'x-dodo-epoch':c.workspaceEpoch } : {}) };
25
+ };
26
+ async function api(path, body, target = false) {
27
+ const response = await fetch(`/api/${path}`, { method:body === undefined ? 'GET':'POST', headers:headers(target), ...(body !== undefined ? { body:JSON.stringify(body) } : {}), cache:'no-store', credentials:'omit' });
28
+ const result = await response.json(); if (!response.ok || result.ok === false) throw new Error(result.error || `HTTP ${response.status}`); return result.data ?? result;
29
+ }
30
+ function notice(text, error = false) {
31
+ $('wb-notice').textContent = `${error ? '✕' : '✓'} ${text}`;
32
+ $('wb-notice').dataset.error = String(error);
33
+ const a = alerts();
34
+ if (a) a.toast(error ? 'error' : 'success', text);
35
+ }
36
+ async function busy(button, action) { if (button.disabled) return; button.disabled = true; button.setAttribute('aria-busy','true'); try { await action(); } catch (e) { notice(e.message,true); } finally { button.disabled = false; button.removeAttribute('aria-busy'); } }
37
+ const button = (text, action, cls = 'btn') => { const b = el('button',text,cls); b.type='button'; b.onclick=() => busy(b,action); return b; };
38
+ function field(form,label,name,value = '',type = 'text') { const l = el('label',label); const i = el(type === 'textarea' ? 'textarea':'input'); i.name=name; if(type !== 'textarea') i.type=type; i.value=String(value); if(type === 'password') { i.autocomplete='new-password'; i.spellcheck=false; } l.append(i); form.append(l); return i; }
39
+ function choice(form,label,name,options,value) { const l=el('label',label), s=el('select'); s.name=name; for(const [id,text] of options) { const o=el('option',text); o.value=id;s.append(o); } if(value!==undefined)s.value=value;l.append(s);form.append(l);return s; }
40
+ function check(form,label,name,checked=false) { const l=el('label',undefined,'wb-check'),i=el('input');i.type='checkbox';i.name=name;i.checked=checked;l.append(i,document.createTextNode(label));form.append(l);return i; }
41
+ const section = (title, text) => { const s=el('section',undefined,'wb-section');s.append(el('h2',title));if(text)s.append(el('p',text,'muted'));return s; };
42
+ function output(parent,value) { const pre=el('pre',typeof value==='string'?value:JSON.stringify(value,null,2),'wb-output');parent.append(pre);return pre; }
43
+
44
+ // ---- confirmations: SweetAlert2 first, shared <dialog id=confirm> fallback ----
45
+ function dialogConfirm(title, text, okLabel) {
46
+ return new Promise((resolve) => {
47
+ const d = $('confirm');
48
+ $('confirm-title').textContent = title;
49
+ $('confirm-text').textContent = text;
50
+ $('confirm-ok').textContent = okLabel || 'ยืนยัน';
51
+ if (typeof d.showModal !== 'function') { resolve(window.confirm(text)); return; }
52
+ const onClose = () => { d.removeEventListener('close', onClose); resolve(d.returnValue === 'ok'); };
53
+ d.addEventListener('close', onClose);
54
+ d.returnValue = 'cancel';
55
+ d.showModal();
56
+ });
57
+ }
58
+ async function confirmDanger(title, text, okLabel) {
59
+ const a = alerts();
60
+ if (a) { const r = await a.confirm({ title, text, confirmText: okLabel || 'ยืนยัน', danger: true }); if (r !== null) return r; }
61
+ return dialogConfirm(title, text, okLabel);
62
+ }
63
+ async function confirmCost(title, text) {
64
+ const a = alerts();
65
+ if (a) { const r = await a.confirmCost({ title, text }); if (r !== null) return r; }
66
+ return dialogConfirm(title, text, 'ยืนยันและยอมรับค่าใช้จ่าย');
67
+ }
68
+
69
+ async function reload() {
70
+ state=await api('ai/state');const picker=$('wb-project');const previous=picker.value;picker.replaceChildren(el('option','เลือกโปรเจกต์…'));
71
+ for(const p of state.projects) { const o=el('option',`${p.displayName} · ${p.available ? 'พร้อม':'ไม่พร้อม'}`);o.value=p.projectId;o.disabled=!p.available;picker.append(o); } if(previous)picker.value=previous;
72
+ $('wb-summary').textContent=`${state.projects.length} โปรเจกต์ · ${state.connections.length} connections · ${state.runs.filter(r=>r.status==='running').length} agents กำลังทำงาน · ${state.accessMode==='personal'?'โหมดส่วนตัว':'โหมดแยกสิทธิ์'}`;
73
+ }
74
+ async function selectProject() { const id=$('wb-project').value;if(!id || !id.startsWith('prj_')) { selected=undefined;return; }selected=await api('ai/project',{projectId:id});$('wb-root').textContent=state.accessMode==='personal'?`${selected.root} · พร้อมใช้ทันทีในโหมดส่วนตัว`:`${selected.root} · บันทึก ${selected.savedTrust} / มีผล ${selected.effectiveTrust}`; }
75
+
76
+ async function show(view) {
77
+ currentView=view; stream?.abort(); UI.tooltips?.hide();
78
+ for(const b of document.querySelectorAll('[data-wb-view]'))b.setAttribute('aria-pressed',String(b.dataset.wbView===view));
79
+ for(const pageEl of document.querySelectorAll('.page'))pageEl.hidden=!(VIEW_PAGES[view]||[]).includes(pageEl.id);
80
+ panel.hidden=false;
81
+ $('wb-toolbar').hidden=!PROJECT_SCOPED.has(view);
82
+ $('wb-root').hidden=!PROJECT_SCOPED.has(view);
83
+ panel.setAttribute('aria-busy','true');
84
+ const body=$('wb-body');
85
+ body.replaceChildren(el('div','กำลังโหลด…','skeleton'));
86
+ try {
87
+ await reload();body.replaceChildren();
88
+ if(view==='overview')overview(body);
89
+ if(view==='projects')await projects(body);
90
+ if(view==='providers'){providers(body);profiles(body);}
91
+ if(view==='chat')chat(body);
92
+ if(view==='runs')runs(body);
93
+ if(view==='knowledge')await knowledge(body);
94
+ if(view==='admin')await admin(body);
95
+ if(view==='settings')await settings(body);
96
+ }
97
+ catch(e){notice(e.message,true);body.replaceChildren(el('p','โหลดไม่ได้ กรุณาตรวจการเชื่อมต่อแล้วลองใหม่'));}finally{panel.removeAttribute('aria-busy');}
98
+ }
99
+ const goTo = (view) => { const b = document.querySelector(`[data-wb-view="${view}"]`); if (b) void busy(b, () => show(view)); };
100
+
101
+ // ---- Overview: status, next steps, and only what needs attention ----------
102
+ function overview(body) {
103
+ const personal = state.accessMode === 'personal';
104
+ const readyProjects = state.projects.filter(p => p.available);
105
+ const readyConnections = state.connections.filter(c => c.enabled && c.credentialPresent);
106
+ const enabledProfiles = state.profiles.filter(p => p.enabled);
107
+ const running = state.runs.filter(r => r.status === 'running').length;
108
+
109
+ const s = section('ภาพรวม', personal
110
+ ? 'โหมดส่วนตัว: เพิ่มโปรเจกต์และ Provider แล้วเริ่มงานได้ทันที ไม่ต้องตั้งสิทธิ์ซ้ำต่อโปรเจกต์'
111
+ : 'โหมดแยกสิทธิ์: กำหนด ACL, trust และ profile allowlist ของแต่ละโปรเจกต์เอง');
112
+ s.querySelector('h2').append(help(personal
113
+ ? 'ในโหมดส่วนตัว OAuth client ที่อนุมัติแล้วใช้ทุกโปรเจกต์ตาม scopes ของ token และทุก profile ที่เปิดอยู่ใช้ได้ทันที ส่วน sandbox, path/secret guards, expected hash และ approval ของงานอันตรายยังทำงานครบ'
114
+ : 'โหมดแยกสิทธิ์เหมาะเมื่อหลาย client ที่เชื่อถือไม่เท่ากันใช้เครื่องเดียวกัน ทุกโปรเจกต์ต้องกำหนด ACL/trust/profile เอง', 'โหมดการใช้งาน'));
115
+ body.append(s);
116
+ const stats = el('div', undefined, 'ov-stats');
117
+ for (const [label, value] of [['โปรเจกต์พร้อมใช้', readyProjects.length], ['Provider connections', readyConnections.length], ['Profiles เปิดใช้', enabledProfiles.length], ['Agents กำลังทำงาน', running]]) {
118
+ const b = el('div', undefined, 'stat');
119
+ b.append(el('div', label, 'stat-label'), el('div', String(value), 'stat-value'));
120
+ stats.append(b);
121
+ }
122
+ s.append(stats);
123
+
124
+ const managedPermissions = personal || (state.permissions || []).length > 0;
125
+ const allReady = readyProjects.length > 0 && readyConnections.length > 0 && enabledProfiles.length > 0 && managedPermissions;
126
+ const steps = [
127
+ { done: readyProjects.length > 0, label: 'เพิ่มโปรเจกต์', detail: 'ระบุโฟลเดอร์ที่จะให้ AI ทำงาน', view: 'projects', action: 'ไปที่โปรเจกต์' },
128
+ { done: readyConnections.length > 0, label: 'เพิ่ม AI Provider', detail: 'เชื่อม OpenAI / Anthropic / Gemini / Ollama พร้อม API key', view: 'providers', action: 'ไปที่ Providers' },
129
+ { done: enabledProfiles.length > 0, label: 'สร้าง Agent Profile', detail: 'เลือกโมเดลและขอบเขตงาน', view: 'providers', action: 'ไปที่ Profiles' },
130
+ ...(personal ? [] : [{ done: (state.permissions || []).length > 0, label: 'กำหนดสิทธิ์ AI ต่อโปรเจกต์', detail: 'เลือก profiles ที่อนุญาตในแต่ละโปรเจกต์', view: 'providers', action: 'ตั้งค่าสิทธิ์' }]),
131
+ { done: allReady && state.runs.length > 0, label: 'พร้อมเริ่มงาน', detail: 'ส่งงานแรกที่ Chat & Tasks', view: 'chat', action: 'เริ่มงาน' },
132
+ ];
133
+ const stepsSection = section('ขั้นตอนถัดไป');
134
+ const list = el('ol', undefined, 'ov-steps');
135
+ steps.forEach((step, index) => {
136
+ const row = el('li', undefined, 'ov-step');
137
+ row.dataset.done = String(step.done);
138
+ row.append(el('span', step.done ? '✓' : String(index + 1), 'num'));
139
+ const grow = el('div', undefined, 'grow');
140
+ grow.append(el('b', step.label), el('small', step.detail));
141
+ row.append(grow);
142
+ const go = el('button', step.action, 'btn small');
143
+ go.type = 'button';
144
+ go.onclick = () => goTo(step.view);
145
+ row.append(go);
146
+ list.append(row);
147
+ });
148
+ stepsSection.append(list);
149
+ body.append(stepsSection);
150
+
151
+ const issues = [];
152
+ for (const p of state.projects.filter(p => !p.available)) issues.push({ text: `โปรเจกต์ ${p.displayName}: ${p.statusText || p.availability}`, view: 'projects' });
153
+ for (const c of state.connections.filter(c => c.enabled && !c.credentialPresent)) issues.push({ text: `Connection ${c.name}: ยังไม่มี API key`, view: 'providers' });
154
+ for (const r of state.runs.filter(r => ['failed', 'interrupted', 'waiting_approval', 'waiting_auth'].includes(r.status))) issues.push({ text: `งาน “${r.task.slice(0, 60)}”: ${STATUS_TH[r.status] || r.status}`, view: 'runs' });
155
+ if (issues.length) {
156
+ const attention = section('ต้องตรวจสอบ');
157
+ const wrap = el('div', undefined, 'ov-issues');
158
+ for (const issue of issues.slice(0, 8)) {
159
+ const row = el('div', undefined, 'callout warn');
160
+ row.append(el('span', '⚠'), el('div', issue.text));
161
+ const go = el('button', 'เปิดดู', 'btn small');
162
+ go.type = 'button';
163
+ go.onclick = () => goTo(issue.view);
164
+ row.append(go);
165
+ wrap.append(row);
166
+ }
167
+ if (issues.length > 8) wrap.append(el('p', `และอีก ${issues.length - 8} รายการ — ดูในหน้าที่เกี่ยวข้อง`, 'muted'));
168
+ attention.append(wrap);
169
+ body.append(attention);
170
+ }
171
+ }
172
+
173
+ async function projects(body) {
174
+ const personal=state.accessMode==='personal';
175
+ const s=section('โปรเจกต์พร้อมกัน',personal?'เพิ่ม path แล้วใช้ได้ทันที ไม่ต้องอนุญาต client หรือ profile ซ้ำ งานต่างโปรเจกต์ทำพร้อมกันได้':'เปิด runtime แยกโปรเจกต์และกำหนด ACL/trust ของแต่ละโปรเจกต์เอง');body.append(s);
176
+ const name=field(s,'ชื่อโปรเจกต์','projectName'),root=field(s,'Absolute path ในเครื่อง','projectRoot');
177
+ s.append(button(personal?'เพิ่มและพร้อมใช้':'เพิ่ม path',async()=>{await api('projects/add',{path:root.value,...(name.value?{displayName:name.value}:{})});notice(personal?'เพิ่มโปรเจกต์แล้ว พร้อมใช้งานทันที':'เพิ่มในทะเบียนแล้ว ตั้งค่าสิทธิ์ก่อนใช้งาน');await show('projects');}));
178
+ if(!selected){s.append(el('p',personal?'เพิ่มหรือเลือกโปรเจกต์จากรายการด้านบน':'เลือกโปรเจกต์จากรายการด้านบนเพื่อจัดการสิทธิ์','wb-empty'));return;}
179
+ if(!personal){
180
+ const trust=choice(s,'Trust ที่บันทึก','trust',[['inspect','inspect — อ่าน; งานที่มีผลขออนุมัติ'],['edit','edit — อ่านและแก้ไฟล์; คำสั่งขออนุมัติ'],['trusted','trusted — รันคำสั่งด้วยสิทธิ์ OS ของผู้ใช้']],selected.savedTrust);
181
+ trust.parentElement.append(help('trust ที่บันทึกคือค่าถาวรของโปรเจกต์ ส่วนสิทธิ์ที่มีผลรอบนี้อาจต่างกันเมื่อ DODO ถูกเปิดด้วย override flag คำสั่งใน trusted ไม่ใช่ OS sandbox', 'trust'));
182
+ s.append(el('p',`สิทธิ์ที่มีผล: ${selected.effectiveTrust}`));
183
+ s.append(button('บันทึก Trust',async()=>{if(trust.value==='trusted'&&!await confirmDanger('บันทึกเป็น trusted?',`คำสั่งจะทำงานด้วยสิทธิ์ OS ของคุณใน ${selected.root}`,'บันทึก trusted'))return;await api('admin/action',{projectId:selected.projectId,operation:'trust.set',args:{mode:trust.value}},true);await selectProject();notice('บันทึกแล้ว มีผล '+selected.effectiveTrust);await show('projects');}));
184
+ s.append(button('ตั้งเป็น default project',async()=>{if(!await confirmDanger('เปลี่ยน default project?',`เปลี่ยน default เป็น ${selected.root} — clients ของ default เดิมต้องอ่าน project_overview ใหม่`,'เปลี่ยน default'))return;await api('ai/project',{projectId:selected.projectId,action:'close'});await api('workspace/switch',{path:selected.root});await reload();await selectProject();notice('เปลี่ยน default workspace แล้ว');await show('projects');}));
185
+ s.append(button('ปิด runtime ที่ว่าง',async()=>{const result=await api('ai/project',{projectId:selected.projectId,action:'close'});notice(result.closed?'ปิด runtime รองที่ว่างแล้ว โปรเจกต์ยังอยู่ในทะเบียน':'ไม่มี runtime รองให้ปิด หรือเป็น default project ที่ยังให้บริการอยู่');}));
186
+ const clients=await api('ai/project/clients',{projectId:selected.projectId});
187
+ const access=section('Client ACL ของโปรเจกต์นี้','แยกอ่าน / แก้ไฟล์ / รันคำสั่ง การอนุญาต profile ไม่เพิ่ม OAuth scopes');s.append(access);
188
+ access.querySelector('h2').append(help('OAuth scopes ของ token เป็นเพดานสูงสุด ACL ต่อโปรเจกต์เลือกได้แคบกว่านั้น การบันทึกไม่ขยาย scope ให้ token ที่ออกไปแล้ว', 'OAuth scopes'));
189
+ if(!clients.length)access.append(el('p','ยังไม่มี OAuth client ลงทะเบียน ใช้หน้า Approvals & Settings เพื่อลงทะเบียน'));
190
+ for(const c of clients){const card=section(c.name,c.clientId);access.append(card);const scopes=['dodo:read','dodo:write','dodo:exec'].map((scope,i)=>[scope,check(card,['อ่านและค้นหา','แก้ไฟล์','รันคำสั่ง'][i],scope,c.scopes.includes(scope))]);card.append(button('บันทึกสิทธิ์',async()=>{const values=scopes.filter(([,b])=>b.checked).map(([scope])=>scope);if(!values.length&&!await confirmDanger('ถอนสิทธิ์ทั้งหมด?',`${c.name} จะเรียก tool ใน ${selected.root} ไม่ได้จนกว่าจะให้สิทธิ์ใหม่ (OAuth login ยังอยู่)`,'ถอนสิทธิ์'))return;await api('ai/project/access',{projectId:selected.projectId,clientId:c.clientId,scopes:values,confirmRevoke:!values.length},true);notice('บันทึก ACL ของโปรเจกต์แล้ว');}));}
191
+ }
192
+ if(personal){
193
+ const ready=section('พร้อมใช้งาน','client ที่คุณอนุมัติใช้โปรเจกต์นี้ได้ทันทีตาม scopes ของ token และทุก profile ที่เปิดอยู่ใช้ได้');
194
+ ready.querySelector('h2').append(help('การเลือก remote profile หมายถึงอนุญาตส่ง context ที่จำเป็นไปยัง provider นั้น คำสั่งรันด้วยสิทธิ์ OS ของคุณและยังผ่าน sandbox/path/secret/hash guards', 'โหมดส่วนตัว'));
195
+ s.append(ready);
196
+ s.append(button('ตั้งเป็น default project',async()=>{await api('ai/project',{projectId:selected.projectId,action:'close'});await api('workspace/switch',{path:selected.root});await reload();await selectProject();notice('เปลี่ยน default workspace แล้ว');await show('projects');}));
197
+ s.append(button('ปิด runtime ที่ว่าง',async()=>{const result=await api('ai/project',{projectId:selected.projectId,action:'close'});notice(result.closed?'ปิด runtime รองแล้ว โปรเจกต์ยังพร้อมเปิดใหม่':'ไม่มี runtime รองที่ปิดได้');}));
198
+ }
199
+ const jobs=section('Jobs ของโปรเจกต์','สถานะจาก runtime ปัจจุบัน');s.append(jobs);
200
+ const jobsDetails=el('details');jobsDetails.append(el('summary','รายละเอียดทางเทคนิค (JSON)'));output(jobsDetails,selected.jobs);jobs.append(jobsDetails);
201
+ }
202
+
203
+ function providers(body) {
204
+ const s=section('AI Providers','เลือก preset หรือ Custom protocol แต่ละ connection เก็บ key แยกกัน การบันทึกไม่เรียกโมเดลและไม่ส่ง source code');body.append(s);
205
+ const f=el('form',undefined,'wb-form');f.onsubmit=e=>e.preventDefault();s.append(f);
206
+ let editing;
207
+ const preset=choice(f,'ผู้ให้บริการ','provider',[...state.presets.map(p=>[p.provider,p.name]),['custom','Custom']],'openai');
208
+ const name=field(f,'ชื่อ connection','name','OpenAI / GPT'), protocol=choice(f,'Protocol','protocol',[['responses','OpenAI Responses'],['chat-completions','OpenAI Chat Completions'],['anthropic','Anthropic Messages'],['gemini','Gemini Interactions'],['ollama','Ollama Native']]);
209
+ const url=field(f,'Base URL (จบที่ /v1 ไม่ใส่ /responses หรือ /chat/completions)','baseUrl','https://api.openai.com/v1'), storage=choice(f,'เก็บ API key','storage',[['session','เฉพาะรอบนี้'],['keychain','macOS Keychain']]), key=field(f,'API key ใหม่ (เว้นว่างเพื่อคงค่าเดิม)','apiKey','','password');
210
+ const privateNet=check(f,'อนุญาต connection นี้เข้าปลายทาง loopback / LAN','private'),enabled=check(f,'เปิดใช้งาน connection','enabled',true);
211
+ const saveState=el('p','ยังไม่ได้บันทึกในรอบนี้','wb-status');saveState.setAttribute('role','status');saveState.setAttribute('aria-live','polite');
212
+ preset.onchange=()=>{const p=state.presets.find(p=>p.provider===preset.value);if(p){name.value=p.name;url.value=p.baseUrl;protocol.value=p.protocol;}};
213
+ f.append(button('บันทึก connection',async()=>{
214
+ if(!name.value.trim()||!url.value.trim()){saveState.dataset.kind='error';saveState.textContent='✕ กรอกชื่อ connection และ Base URL ก่อนบันทึก';throw new Error('กรอกชื่อ connection และ Base URL ก่อนบันทึก');}
215
+ const apiKey=key.value;key.value='';
216
+ saveState.dataset.kind='';saveState.textContent=`กำลังบันทึก ${name.value}…`;
217
+ try{
218
+ await api('ai/connection',{connection:{...(editing?{id:editing}:{}),name:name.value,provider:preset.value,protocol:protocol.value,baseUrl:url.value,enabled:enabled.checked,allowPrivateNetwork:privateNet.checked,credentialStorage:storage.value},...(apiKey?{apiKey}:{})});
219
+ saveState.dataset.kind='ok';saveState.textContent=`✓ บันทึก ${name.value} แล้ว · ${timeText()} (backend ยืนยัน)`;
220
+ notice('บันทึก connection แล้ว');await show('providers');
221
+ }catch(e){saveState.dataset.kind='error';saveState.textContent=`✕ บันทึกไม่สำเร็จ: ${e.message} · ${timeText()}`;alerts()?.error({title:'บันทึก connection ไม่สำเร็จ',text:e.message});throw e;}
222
+ },'btn primary'));
223
+ f.append(saveState);
224
+ if(!state.connections.length)s.append(el('p','ยังไม่มี provider เพิ่ม connection เพื่อเริ่มต้น','wb-empty'));
225
+ for(const c of state.connections){const card=section(c.name,`${c.protocol} · ${c.enabled?'เปิดใช้งาน':'ปิดใช้งาน'} · key ${c.credentialPresent?'มีแล้ว':'ยังไม่มี/ต้องใส่ใหม่'}`);card.append(el('code',c.baseUrl));
226
+ card.append(button('แก้ไข',()=>{editing=c.id;preset.value=c.provider;name.value=c.name;url.value=c.baseUrl;protocol.value=c.protocol;storage.value=c.credentialStorage;privateNet.checked=c.allowPrivateNetwork;enabled.checked=c.enabled;key.value='';name.focus();}));
227
+ const modelState=el('div',c.credentialPresent?'ยังไม่ได้โหลดรายชื่อโมเดลในรอบนี้':'ต้องใส่ API key ก่อนโหลดรายชื่อโมเดล','wb-model-state');modelState.setAttribute('role','status');modelState.setAttribute('aria-live','polite');
228
+ card.append(button('โหลดรายชื่อโมเดล',async()=>{modelState.classList.remove('error');modelState.textContent=`กำลังโหลดรายชื่อโมเดลจาก ${c.name}… (credential test)`;try{const models=await api('ai/models',{connectionId:c.id});modelState.replaceChildren();if(!models.length){modelState.textContent=`Endpoint ของ ${c.name} ไม่ส่ง Model ID กลับมา (ทดสอบเมื่อ ${timeText()}) กรุณากรอก Model ID เองในส่วน Agent Profiles`;notice('ไม่พบ Model ID จาก endpoint',true);return;}const title=el('p',`พบ ${models.length} โมเดล จาก ${c.name} · credential ใช้ได้ · ${timeText()} — เลือก Model ID ไปใส่ในส่วน Agent Profiles`);const list=el('ul',undefined,'wb-model-list');for(const item of models){const id=String(item.id||'');if(!id)continue;const row=el('li');row.append(el('code',id),button('คัดลอก',async()=>{await navigator.clipboard.writeText(id);notice(`คัดลอก Model ID ${id} แล้ว`);}));list.append(row);}modelState.append(title,list);notice(`โหลดรายชื่อโมเดลแล้ว ${models.length} รายการ`);}catch(e){modelState.classList.add('error');modelState.textContent=`โหลดรายชื่อโมเดลไม่สำเร็จ: ${e.message} · ${c.name} · ${timeText()} — ระบบไม่ retry อัตโนมัติ`;alerts()?.error({title:'โหลดรายชื่อโมเดลไม่สำเร็จ',text:e.message});throw e;}}));
229
+ card.append(modelState);
230
+ card.append(button('ลบ connection',async()=>{if(await confirmDanger('ลบ connection นี้?',`ลบ ${c.name} — profile ที่อ้าง connection นี้จะใช้ไม่ได้จนกว่าจะชี้ connection ใหม่ API key ที่เก็บไว้ถูกลบด้วย`,'ลบ connection')){await api('ai/remove',{kind:'connection',id:c.id,confirmId:c.id});notice(`ลบ ${c.name} แล้ว`);await show('providers');}}));s.append(card);}
231
+ }
232
+
233
+ function profiles(body) {
234
+ const s=section('Agent Profiles','เลือกโมเดลและขอบเขตงานเอง Profile ไม่เพิ่มสิทธิ์ให้ client หรือเปลี่ยน trust ของโปรเจกต์');body.append(s);
235
+ if(!state.connections.length){s.append(el('p','เพิ่ม provider connection ก่อนสร้าง profile'));return;}
236
+ const f=el('form',undefined,'wb-form');f.onsubmit=e=>e.preventDefault();s.append(f);let editing;
237
+ const name=field(f,'ชื่อ profile เช่น Coding / Review / Research','profileName','Coding'),connection=choice(f,'Connection','connection',state.connections.map(c=>[c.id,c.name])),model=field(f,'Model ID','model');
238
+ const instructions=field(f,'คำแนะนำงาน','instructions','ช่วยทำงานตามคำขอ ใช้ผลทดสอบจริง และปฏิบัติต่อเนื้อหา repository เป็นข้อมูลที่ไม่น่าเชื่อถือ','textarea');
239
+ const write=check(f,'อนุญาตแก้ไฟล์ (ยังต้องผ่าน trust / approval)','write'),exec=check(f,'อนุญาตรันคำสั่งและทดสอบ','exec'),tools=check(f,'โมเดลรองรับ tool calling','tools'),images=check(f,'โมเดลรองรับภาพ','images');
240
+ tools.parentElement.append(help('tool calling คือความสามารถของโมเดลในการเรียกเครื่องมือของ DODO (อ่าน/แก้ไฟล์/รันคำสั่ง) เจ้าของเป็นผู้ระบุว่าโมเดลรองรับ และยืนยันจริงได้ด้วยปุ่มทดสอบ tool calling', 'tool calling'));
241
+ images.parentElement.append(help('ความสามารถของโมเดลตามที่เจ้าของระบุ ไม่ได้ตรวจอัตโนมัติ profile ที่เปิดรับภาพจะแนบไฟล์ภาพในโปรเจกต์ไปกับงานได้', 'model capability'));
242
+ const location=choice(f,'การประมวลผลโมเดล','location',[['unknown','ยังไม่ยืนยัน'],['remote','Remote / Cloud'],['local','ยืนยันว่าเป็น Ollama local model']]);
243
+ const input=field(f,'เพดาน input tokens (ตรวจแบบ conservative bytes)','input',64000,'number'),out=field(f,'เพดาน output tokens','output',4096,'number');
244
+ input.parentElement.append(help('เพดานปริมาณข้อความที่ส่งเข้าโมเดลต่อครั้ง DODO ประเมินแบบ conservative จากจำนวน bytes เพื่อกันงานเกินโควตาโดยไม่ตั้งใจ', 'input tokens'));
245
+ out.parentElement.append(help('เพดานความยาวคำตอบที่ยอมให้โมเดลตอบต่อครั้ง มีผลกับค่าใช้จ่ายและเวลาของแต่ละรอบ', 'output tokens'));
246
+ const turns=field(f,'จำนวนรอบเรียกโมเดลสูงสุด','turns',20,'number'),actions=field(f,'Tool actions สูงสุด','actions',50,'number'),timeout=field(f,'เวลารวมของ run (นาที)','timeout',30,'number');
247
+ const inputPrice=field(f,'ราคา input ต่อ 1M tokens (ว่าง = ไม่ทราบ)','inputPrice','','number'),outputPrice=field(f,'ราคา output ต่อ 1M tokens (ว่าง = ไม่ทราบ)','outputPrice','','number');inputPrice.step=outputPrice.step='any';
248
+ const enabled=check(f,'เปิดใช้ profile','profileEnabled',true);
249
+ f.append(button('บันทึก profile',async()=>{await api('ai/profile',{...(editing?{id:editing}:{}),name:name.value,connectionId:connection.value,model:model.value,instructions:instructions.value,scopes:['dodo:read',...(write.checked?['dodo:write']:[]),...(exec.checked?['dodo:exec']:[])],toolCalling:tools.checked,imageInput:images.checked,inferenceLocation:location.value,maxInputTokens:Number(input.value),maxOutputTokens:Number(out.value),maxTurns:Number(turns.value),maxActions:Number(actions.value),timeoutMinutes:Number(timeout.value),enabled:enabled.checked,...(inputPrice.value?{inputPricePerMillion:Number(inputPrice.value)}:{}),...(outputPrice.value?{outputPricePerMillion:Number(outputPrice.value)}:{})});notice('บันทึก profile แล้ว');await show('providers');},'btn primary'));
250
+ for(const p of state.profiles){const conn=state.connections.find(c=>c.id===p.connectionId);const card=section(p.name,`${p.model} · ${conn?conn.name:'connection ถูกลบ'} · ${p.scopes.join(', ')}`);card.append(button('แก้ไข',()=>{editing=p.id;name.value=p.name;connection.value=p.connectionId;model.value=p.model;instructions.value=p.instructions;write.checked=p.scopes.includes('dodo:write');exec.checked=p.scopes.includes('dodo:exec');tools.checked=p.toolCalling;images.checked=p.imageInput;location.value=p.inferenceLocation;input.value=p.maxInputTokens;out.value=p.maxOutputTokens;turns.value=p.maxTurns;actions.value=p.maxActions;timeout.value=p.timeoutMinutes;enabled.checked=p.enabled;inputPrice.value=p.inputPricePerMillion??'';outputPrice.value=p.outputPricePerMillion??'';name.focus();}));
251
+ card.append(el('p',`Text · ภาพ ${p.imageInput?'เจ้าของระบุว่ารองรับ':'ไม่เปิดใช้'} · Tool calling ${p.toolCalling?'เจ้าของระบุว่ารองรับ':'ไม่เปิดใช้'} · การประมวลผล ${p.inferenceLocation}`,'muted'));
252
+ const probeText=()=>['inference','tools'].map(mode=>{const hit=state.probes.find(t=>t.profileId===p.id&&t.mode===mode);return hit?`${mode} ผ่านเมื่อ ${new Date(hit.checkedAt).toLocaleString('th-TH')}`:`${mode} ยังไม่ได้ทดสอบ`;}).join(' · ')+' (ผลเก่าไม่รับรองโมเดล/ค่าที่เปลี่ยนภายหลัง)';
253
+ const probeStatus=el('p',`ผลทดสอบของ ${conn?conn.name:'?'} · ${p.model}: ${probeText()}`);probeStatus.setAttribute('role','status');probeStatus.setAttribute('aria-live','polite');card.append(probeStatus);
254
+ const probeLive=el('div','พร้อมทดสอบด้วยข้อมูลสังเคราะห์ โดยไม่ส่ง source code','wb-model-state');probeLive.setAttribute('role','status');probeLive.setAttribute('aria-live','assertive');card.append(probeLive);
255
+ if(conn?.protocol==='ollama')card.append(button('ตรวจ metadata / local model (ไม่ทำ inference)',async()=>output(card,await api('ai/metadata',{profileId:p.id}))));
256
+ for(const [mode,label] of [['inference','ทดสอบข้อความ (มีค่าใช้จ่ายได้)'],['tools','ทดสอบ tool calling (มีค่าใช้จ่ายได้)']])card.append(button(label,async()=>{
257
+ if(!await confirmCost('เรียกโมเดลจริง อาจมีค่าใช้จ่าย',`ทดสอบ ${mode} กับ ${conn?conn.name:'connection'} · โมเดล ${p.model} ด้วยข้อมูลสังเคราะห์ ไม่ส่ง source code ค่าใช้จ่ายไม่ทราบล่วงหน้า (ไม่แสดงเป็นศูนย์) และระบบจะไม่ retry อัตโนมัติ`))return;
258
+ probeLive.classList.remove('error');probeLive.textContent=`กำลังทดสอบ ${mode}… อาจใช้เวลาสูงสุด 60 วินาที (ไม่ retry อัตโนมัติ)`;
259
+ const closeLoading=alerts()?alerts().loading(`กำลังทดสอบ ${mode}`,`${conn?conn.name:''} · ${p.model} — สูงสุด 60 วินาที ไม่ retry อัตโนมัติ`):()=>undefined;
260
+ try{const result=await api('ai/probe',{profileId:p.id,mode,confirmUsage:true});closeLoading();state.probes=state.probes.filter(t=>!(t.profileId===p.id&&t.mode===mode));state.probes.push(result);probeStatus.textContent=`ผลทดสอบของ ${conn?conn.name:'?'} · ${p.model}: ${probeText()}`;probeLive.textContent=`✓ ${mode} ผ่านการทดสอบจริง · ${conn?conn.name:''} · ${p.model} · ${timeText(result.checkedAt)}`;const details=el('details');details.append(el('summary','รายละเอียดทางเทคนิค (JSON)'));output(details,result);card.append(details);notice(`${mode} ผ่านการทดสอบจริงแล้ว`);}
261
+ catch(e){closeLoading();probeLive.classList.add('error');probeLive.textContent=`✕ ${mode} ไม่ผ่าน: ${e.message} · ${timeText()} — ระบบไม่ retry อัตโนมัติ สถานะฝั่ง provider อาจไม่แน่นอน`;alerts()?.error({title:`ทดสอบ ${mode} ไม่ผ่าน`,text:`${e.message} — ระบบไม่ retry อัตโนมัติ`});throw e;}
262
+ }));
263
+ card.append(button('ลบ profile',async()=>{if(await confirmDanger('ลบ profile นี้?',`ลบ ${p.name} — งานใหม่จะใช้ profile นี้ไม่ได้อีก ประวัติงานเดิมไม่ถูกลบ`,'ลบ profile')){await api('ai/remove',{kind:'profile',id:p.id,confirmId:p.id});notice(`ลบ ${p.name} แล้ว`);await show('providers');}}));s.append(card);}
264
+ if(state.accessMode==='personal'){body.append(section('พร้อมใช้ทุกโปรเจกต์','โหมดส่วนตัวใช้ profile ที่เปิดอยู่กับทุกโปรเจกต์ทันที ไม่ต้องทำ profile allowlist, client allowlist หรือ source-egress permission ซ้ำ'));return;}
265
+ const permissions=section('สิทธิ์ AI ของโปรเจกต์','เลือกโปรเจกต์ด้านบน แล้วระบุ profiles และ clients ที่อนุญาตให้ใช้ API นี้');body.append(permissions);
266
+ const boxes=state.profiles.map(p=>[p.id,check(permissions,p.name,p.id,state.permissions.find(x=>x.projectId===selected?.projectId)?.profileIds.includes(p.id))]);
267
+ const egress=check(permissions,'อนุญาตส่ง source/context ของโปรเจกต์นี้ไปยัง provider ที่เลือก','egress',state.permissions.find(x=>x.projectId===selected?.projectId)?.allowSourceEgress);
268
+ egress.parentElement.append(help('source egress คือการยอมให้เนื้อหาโปรเจกต์ (โค้ด, บริบท) ออกไปยัง AI provider ภายนอก ปิดไว้ = profile remote ใช้กับโปรเจกต์นี้ไม่ได้', 'source egress'));
269
+ const clients=field(permissions,'OAuth Client IDs ที่อนุญาต spawn (หนึ่ง ID ต่อบรรทัด)','clients',(state.permissions.find(x=>x.projectId===selected?.projectId)?.allowedClientIds||[]).join('\n'),'textarea');
270
+ permissions.append(button('บันทึกสิทธิ์ AI',async()=>{if(!selected)throw new Error('เลือกโปรเจกต์ก่อน');await api('ai/permission',{projectId:selected.projectId,profileIds:boxes.filter(([,b])=>b.checked).map(([id])=>id),allowSourceEgress:egress.checked,allowedClientIds:clients.value.split(/\s+/).filter(Boolean)});notice('บันทึกสิทธิ์ AI แล้ว');await reload();},'btn primary'));
271
+ }
272
+
273
+ function chat(body) {
274
+ const s=section('Chat & Tasks','ส่งงานให้ profile ที่เจ้าของอนุญาต งานทำต่อได้เมื่อปิด browser และต้อง Resume เองหลัง DODO restart');body.append(s);
275
+ const f=el('form',undefined,'wb-form');f.onsubmit=e=>e.preventDefault();s.append(f);
276
+ const allowed=state.accessMode==='personal'?state.profiles.map(p=>p.id):(state.permissions.find(p=>p.projectId===selected?.projectId)?.profileIds||[]);
277
+ const profile=choice(f,'Agent Profile','profile',state.profiles.filter(p=>p.enabled&&allowed.includes(p.id)).map(p=>[p.id,`${p.name} · ${p.model}`]));
278
+ if(!profile.options.length)s.append(el('p','ยังไม่มี profile ที่อนุญาตสำหรับโปรเจกต์นี้ ตั้งค่าที่ Providers & Profiles ก่อน','wb-empty'));
279
+ const prior=choice(f,'บทสนทนา','prior',[['','เริ่มงานใหม่'],...state.runs.filter(r=>r.status==='completed'&&r.projectId===selected?.projectId).map(r=>[r.id,'คุยต่อ: '+r.task.slice(0,60)])],followRun||'');
280
+ prior.onchange=()=>{const r=state.runs.find(r=>r.id===prior.value);if(r)profile.value=r.profileId;};prior.onchange();
281
+ const task=field(f,'ข้อความ / งานที่ต้องการให้ AI ทำ','task','','textarea');task.rows=5;
282
+ const imagePaths=field(f,'ภาพที่แนบ: path ภายในโปรเจกต์ (หนึ่งไฟล์ต่อบรรทัด ไม่เกิน 4)','images','','textarea');
283
+ let pendingKey;
284
+ const send=button('ส่งงาน',async()=>{if(!selected)throw new Error('เลือกโปรเจกต์ด้านบนก่อน');if(!task.value.trim())throw new Error('กรอกงานที่ต้องการ');pendingKey ||= crypto.randomUUID();const r=await api('ai/runs',{projectId:selected.projectId,profileId:profile.value,task:task.value,idempotencyKey:pendingKey,...(prior.value?{parentRunId:prior.value}:{}),images:imagePaths.value.split('\n').map(x=>x.trim()).filter(Boolean).map(path=>({path}))},true);pendingKey=undefined;followRun=r.id;notice('สร้างงานแล้ว');await watch(r.id,s);},'btn primary');
285
+ f.append(send);
286
+ const idem=el('p','ปุ่มนี้กันการส่งซ้ำ: การกดซ้ำหรือ reconnect ระหว่างส่งจะไม่สร้างงานใหม่','muted small-text');
287
+ idem.append(help('idempotency key คือรหัสประจำคำขอที่เบราว์เซอร์สร้างครั้งเดียวต่อการส่ง หากคำขอเดิมถูกส่งซ้ำ (เช่นเน็ตสะดุด) server จะคืนงานเดิมแทนการสร้างใหม่', 'idempotency'));
288
+ f.append(idem);
289
+ }
290
+
291
+ function runs(body) {
292
+ const s=section('Runs & Jobs','ประวัติอยู่ในเครื่อง แสดงผลที่โมเดลและ tools ส่งกลับจริง ค่าใช้จ่ายที่ไม่ทราบจะแสดง “ไม่ทราบ” ไม่ใช่ศูนย์');body.append(s);
293
+ if(!state.runs.length)s.append(el('p','ยังไม่มีงาน AI','wb-empty'));
294
+ const search=field(s,'ค้นหาประวัติ','search');s.append(button('ค้นหา',async()=>{state.runs=await api('ai/runs?search='+encodeURIComponent(search.value));body.replaceChildren();runs(body);}));
295
+ for(const r of state.runs){const card=section(r.task.slice(0,100),`${STATUS_TH[r.status]||r.status} · ${r.actions} actions · ${r.modelCalls} model calls · ค่าใช้จ่าย ${r.estimatedCost===null?'ไม่ทราบ':r.estimatedCost.toFixed(6)}`);card.append(button('เปิดผลและเหตุการณ์',()=>watch(r.id,card)));
296
+ for(const [action,label] of [['pause','พัก'],['resume','Resume'],['cancel','ยกเลิก'],['delete','ลบประวัติ']])card.append(button(label,async()=>{
297
+ if(action==='cancel'&&!await confirmDanger('ยกเลิกงานนี้?',`หยุด “${r.task.slice(0,80)}” — สิ่งที่ทำไปแล้วไม่ย้อนกลับอัตโนมัติ ตรวจ diff ในโปรเจกต์ได้จาก receipts`,'ยกเลิกงาน'))return;
298
+ if(action==='delete'&&!await confirmDanger('ลบประวัติงานนี้?',`ลบประวัติ “${r.task.slice(0,80)}” ออกจากเครื่อง ไฟล์ในโปรเจกต์ไม่ถูกลบ`,'ลบประวัติ'))return;
299
+ await api(`ai/runs/${r.id}/control`,{action});notice(action==='cancel'?'ยกเลิกงานแล้ว':action==='delete'?'ลบประวัติแล้ว':action==='pause'?'พักงานแล้ว':'สั่ง Resume แล้ว');await show('runs');}));s.append(card);}
300
+ }
301
+
302
+ async function watch(id,parent) {
303
+ stream?.abort();stream=new AbortController();const controller=stream,signal=controller.signal;
304
+ const statusLine=el('p','กำลังเชื่อมต่อเหตุการณ์…','muted');statusLine.setAttribute('role','status');parent.append(statusLine);
305
+ const answer=el('div',undefined,'wb-answer');answer.hidden=true;parent.append(answer);
306
+ const receipts=el('div',undefined,'wb-receipts');parent.append(receipts);
307
+ function record(event) {
308
+ const p=event.payload;
309
+ if(event.kind==='text'){text=(text+p.text).slice(-50000);answer.textContent=text;answer.hidden=false;return;}
310
+ if(event.kind==='completed'){answer.textContent=p.result||text;answer.hidden=false;return;}
311
+ const titles={queued:'รับงานเข้าคิวแล้ว',context:p.ok?'✓ โหลด context แล้ว':'Context: ตรวจรายละเอียด',tool:`${p.ok?'✓':'✕'} ${p.operation||'Tool'}${p.error?' · '+p.error.code:''}`,job:`คำสั่ง: ${p.status} · exit ${p.exitCode??'ยังไม่ทราบ'}`,error:`✕ ${p.code||'งานหยุด'}: ${p.message||''}`};
312
+ const card=el('details',undefined,'wb-receipt');card.append(el('summary',titles[event.kind]||event.kind));
313
+ if(event.kind==='job'){output(card,p.stdout||'(ไม่มี stdout)');if(p.stderr)output(card,p.stderr);}
314
+ else output(card,JSON.stringify(p,null,2).slice(0,16000));
315
+ receipts.append(card);while(receipts.children.length>100)receipts.firstElementChild.remove();
316
+ }
317
+ let cursor=0,text='',failures=0;
318
+ void (async()=>{
319
+ while(!signal.aborted && failures<5){
320
+ try {
321
+ const res=await fetch(`/api/ai/runs/${encodeURIComponent(id)}/events?after=${cursor}`,{headers:headers(),signal,cache:'no-store'});
322
+ if(!res.ok)throw new Error(`เปิด events ไม่สำเร็จ (${res.status})`);
323
+ const reader=res.body.getReader(),decoder=new TextDecoder();let pending='';
324
+ statusLine.textContent='เชื่อมต่อแล้ว · กำลังรับผลจริง';failures=0;
325
+ for(;;){
326
+ const {done,value}=await reader.read();if(done)break;
327
+ pending+=decoder.decode(value,{stream:true});let index;
328
+ while((index=pending.indexOf('\n\n'))>=0){
329
+ const packet=pending.slice(0,index);pending=pending.slice(index+2);const line=packet.split('\n').find(l=>l.startsWith('data: '));if(!line)continue;
330
+ const event=JSON.parse(line.slice(6));if(event.seq<=cursor)continue;cursor=event.seq;
331
+ record(event);
332
+ if(event.kind==='completed'){statusLine.textContent='งานเสร็จแล้ว · ตรวจ diff และผลทดสอบใน receipts ด้านล่าง';await reader.cancel();return;}
333
+ }
334
+ }
335
+ const status=await api(`ai/runs/${id}`);statusLine.textContent=`สถานะ: ${STATUS_TH[status.status]||status.status}`;
336
+ if(['completed','failed','canceled','paused','interrupted','waiting_approval','waiting_auth'].includes(status.status))return;
337
+ } catch(e){if(signal.aborted)return;statusLine.textContent=e.message;}
338
+ failures++;statusLine.textContent+=` · เชื่อมต่อใหม่ครั้งที่ ${failures} (ไม่ส่งงานซ้ำ)`;
339
+ await new Promise(r=>setTimeout(r,Math.min(5000,failures*1000)));
340
+ }
341
+ })();
342
+ }
343
+
344
+ async function admin(body) {
345
+ const s=section('Approvals & Administration','คำสั่งในหน้านี้ใช้สิทธิ์เจ้าของและ validation เดียวกับ CLI ตรวจ project และรายละเอียดก่อนอนุมัติ');body.append(s);
346
+ const f=el('form',undefined,'wb-form');f.onsubmit=e=>e.preventDefault();s.append(f);
347
+ const operation=choice(f,'การจัดการ','operation',state.ownerActions.map(a=>[a,a])),argumentsField=field(f,'รายละเอียดคำสั่ง JSON (เช่น {"id":"…","digest":"…"})','args','{}','textarea');argumentsField.rows=5;
348
+ f.append(button('เรียกดู / ดำเนินการ',async()=>{if(!selected)throw new Error('เลือกโปรเจกต์ก่อน');const args=JSON.parse(argumentsField.value);if(/approve|review|revoke|resolve|prune|set|policy/.test(operation.value)&&!await confirmDanger('ยืนยันคำสั่งเจ้าของ?',`${operation.value} ของโปรเจกต์ ${selected.root}`,'ดำเนินการ'))return;output(s,await api('admin/action',{projectId:selected.projectId,operation:operation.value,args},true));}));
349
+ for(const [op,label] of [['approvals.pending','คำขอ actions'],['auth.pending','OAuth consent'],['schedule.list','Schedules'],['memory.pending','Memory ที่รอ review'],['agent.skill.pending','Skills ที่รอ review']])s.append(button(label,async()=>{if(!selected)throw new Error('เลือกโปรเจกต์ก่อน');const card=section(label);s.append(card);output(card,await api('admin/action',{projectId:selected.projectId,operation:op,args:{}},true));}));
350
+ }
351
+
352
+ async function knowledge(body) {
353
+ const s=section('Knowledge','Context, Project Brain และ approved memory/skills แยกตามโปรเจกต์ ประวัติ AI ไม่กลายเป็น approved memory อัตโนมัติ');body.append(s);
354
+ if(!selected){s.append(el('p','เลือกโปรเจกต์ก่อน','wb-empty'));return;}
355
+ for(const [operation,title] of [['brain_status','Project Brain'],['context_status','Context Engine'],['memory_status','Memory']]){
356
+ const result=await api('ai/knowledge',{projectId:selected.projectId,operation},true),card=section(title,result.ok?'✓ โหลดสถานะแล้ว':'✕ '+(result.error?.message||'โหลดไม่สำเร็จ'));s.append(card);const details=el('details');details.append(el('summary','รายละเอียดสถานะ'));output(details,result.data||result.error);card.append(details);
357
+ }
358
+ const query=field(s,'ค้นหา memory หรือ reviewed skills','query');for(const [operation,title] of [['memory_search','ค้นหา Memory'],['agent_skill_search','ค้นหา Skills']])s.append(button(title,async()=>{const result=await api('ai/knowledge',{projectId:selected.projectId,operation,query:query.value},true);output(s,result.data||result.error);}));
359
+ }
360
+
361
+ async function settings(body) {
362
+ const config=await api('admin/config');
363
+ const access=section('โหมดการใช้งาน',state.accessMode==='personal'?'โหมดส่วนตัว: เพิ่มโปรเจกต์/โมเดลแล้วใช้ได้ทันที และคำสั่งทำงานด้วยสิทธิ์ OS ของบัญชีคุณ':'โหมดแยกสิทธิ์: ตั้ง ACL, trust, profile และ source egress ต่อโปรเจกต์');body.append(access);
364
+ access.querySelector('h2').append(help('Personal = คนเดียวใช้ ทุก client ที่อนุมัติแล้วใช้ทุกโปรเจกต์ตาม token scopes · Managed = แยกสิทธิ์ราย client รายโปรเจกต์ การสลับโหมดไม่แตะ OAuth scopes, sandbox หรือ guards ใด ๆ', 'Personal / Managed mode'));
365
+ if(state.accessMode==='personal')access.append(button('เปลี่ยนเป็นโหมดแยกสิทธิ์',async()=>{if(!await confirmDanger('เปลี่ยนเป็นโหมดแยกสิทธิ์?','หลังเปลี่ยน AI จะหยุดจนกว่าจะตั้ง ACL/trust/profile permission ของแต่ละโปรเจกต์','เปลี่ยนโหมด'))return;await api('ai/access-mode',{mode:'managed'});notice('เปิดโหมดแยกสิทธิ์แล้ว');await show('settings');}));
366
+ else access.append(button('ใช้โหมดส่วนตัวแบบเพิ่มแล้วใช้ได้เลย',async()=>{if(!await confirmDanger('เปลี่ยนเป็นโหมดส่วนตัว?','OAuth client ที่คุณอนุมัติจะใช้ทุกโปรเจกต์ตาม token scopes และ remote profiles จะส่ง context ไป provider ได้','เปลี่ยนโหมด'))return;await api('ai/access-mode',{mode:'personal'});notice('เปิดโหมดส่วนตัวแล้ว มีผลทันที');await show('settings');},'btn primary'));
367
+ const exposure=section('Sub-agent tools ใน MCP',config.exposeSubagentsToMcp?'เปิดอยู่: AI ภายนอกมองเห็น operations สำหรับสร้างและจัดการ sub-agent':'ปิดอยู่: หน้าเว็บยังสร้าง agent ได้ แต่ MCP clients จะไม่เห็น operations กลุ่ม sub-agent');body.append(exposure);
368
+ const exposureSummary=exposure.querySelector(':scope > p.muted');
369
+ exposure.querySelector('h2').append(help('ควบคุมเฉพาะ MCP catalog: subagent_spawn, subagent_status, subagent_result และ subagent_control รวมถึงรายการใน dodo_discover/gateway schema ไม่เพิ่มหรือลด OAuth scopes, trust, approvals หรือสิทธิ์ของ profile', 'Sub-agent tool exposure'));
370
+ const exposeSubagents=check(exposure,'เปิดให้ MCP clients เห็น Sub-agent tools','exposeSubagentsToMcp',Boolean(config.exposeSubagentsToMcp));
371
+ exposeSubagents.parentElement.classList.add('wb-switch');
372
+ const exposureState=el('p',config.exposeSubagentsToMcp?'สถานะที่บันทึก: เปิด':'สถานะที่บันทึก: ปิด','muted');exposure.append(exposureState);
373
+ exposure.append(button('บันทึกการมองเห็น tools',async()=>{const next=exposeSubagents.checked;if(next&&!await confirmDanger('เปิด Sub-agent tools ให้ MCP?', 'AI clients จะมองเห็นและสามารถขอสร้าง agent ได้ แต่ทุกคำขอยังต้องผ่าน exec scope, project context, profile policy และ approval เดิม', 'เปิดและบันทึก'))return;const result=await api('admin/config',{exposeSubagentsToMcp:next});if(exposureSummary)exposureSummary.textContent=next?'เปิดอยู่: AI ภายนอกมองเห็น operations สำหรับสร้างและจัดการ sub-agent':'ปิดอยู่: หน้าเว็บยังสร้าง agent ได้ แต่ MCP clients จะไม่เห็น operations กลุ่ม sub-agent';exposureState.textContent=`สถานะที่บันทึก: ${next?'เปิด':'ปิด'} · ต้อง restart DODO และ rescan/recreate MCP app`;notice(result.restartRequired?'บันทึกแล้ว — restart DODO และ rescan MCP client เพื่อให้ catalog ใหม่มีผล':'บันทึกแล้ว');},'btn primary'));
374
+ exposure.append(el('p','ค่าเริ่มต้นคือปิด การตั้งค่านี้ไม่กระทบ Chat & Tasks บนหน้าเว็บ และไม่หยุด agent ที่กำลังทำงานอยู่','muted small-text'));
375
+ const limits=section('ขีดจำกัด AI และประวัติ','เปลี่ยนเพดานสำหรับงานใหม่ งานที่กำลังรันจะไม่ถูกยกเลิกโดยเงียบ ๆ');body.append(limits);
376
+ limits.querySelector('h2').append(help('retention คืออายุของประวัติงานที่จบแล้วก่อนถูกลบเมื่อกดล้างตามอายุ ประวัติที่ค้าง/รออนุมัติไม่ถูกลบตามอายุ', 'retention'));
377
+ const limitFields=Object.entries(state.limits).map(([k,v])=>[k,field(limits,({global:'Agents พร้อมกันทั้งหมด',perProject:'Agents ต่อโปรเจกต์',ollama:'ต่อ Ollama connection',queued:'จำนวนงานรอคิว',retentionDays:'อายุประวัติงานที่จบแล้ว (วัน)'})[k]||k,k,v,'number')]);
378
+ limits.append(button('บันทึกขีดจำกัด',async()=>{await api('ai/limits',Object.fromEntries(limitFields.map(([k,f])=>[k,Number(f.value)])));notice('บันทึกขีดจำกัดแล้ว');}));
379
+ limits.append(button('ลบประวัติเก่าตาม retention',async()=>{if(await confirmDanger('ลบประวัติเก่า?','ลบประวัติงานที่จบแล้วและเก่ากว่าที่กำหนด การลบย้อนกลับไม่ได้','ลบประวัติเก่า'))output(limits,await api('ai/prune',{}));}));
380
+ const prefs=section('Advanced Settings','แก้เฉพาะเมื่อเข้าใจผลกระทบ การเปลี่ยน global runtime settings ต้อง restart DODO');body.append(prefs);
381
+ const prefsDetails=el('details');prefsDetails.append(el('summary','รายละเอียดทางเทคนิค (JSON configuration)'));prefs.append(prefsDetails);
382
+ const json=field(prefsDetails,'การตั้งค่าระบบ JSON','config',JSON.stringify(config,null,2),'textarea');json.rows=12;prefsDetails.append(button('บันทึก Settings',async()=>{const result=await api('admin/config',JSON.parse(json.value));notice(result.restartRequired?'บันทึกแล้ว ต้อง restart เพื่อใช้ค่าใหม่':'บันทึกแล้ว');}));
383
+ const setup=section('Setup & Doctor','ตรวจส่วนประกอบหรือดูแผนติดตั้งก่อน ยืนยันเมื่อจะดาวน์โหลด/ติดตั้งจริง การให้สิทธิ์ macOS ยังทำผ่านหน้าต่างระบบ');body.append(setup);const components=field(setup,'ส่วนประกอบ คั่นด้วย comma','components','git,ripgrep,cloudflared,ffmpeg,whisper,chromium');for(const [mode,label] of [['check','ตรวจเครื่อง'],['plan','ดูแผนติดตั้ง'],['install','ติดตั้งที่เลือก']])setup.append(button(label,async()=>{if(mode==='install'&&!await confirmDanger('ติดตั้งส่วนประกอบ?','ดาวน์โหลดและติดตั้งส่วนประกอบที่เลือกลงเครื่องนี้','ติดตั้ง'))return;output(setup,await api('admin/setup',{mode,components:components.value.split(',').map(v=>v.trim()).filter(Boolean),confirmInstall:mode==='install'}));}));
384
+ }
385
+
386
+ for(const b of document.querySelectorAll('[data-wb-view]'))b.onclick=()=>busy(b,()=>show(b.dataset.wbView));
387
+ $('wb-project').onchange=()=>busy($('wb-select'),async()=>{await selectProject();await show(currentView||'projects');});
388
+ $('wb-select').onclick=()=>busy($('wb-select'),async()=>{await selectProject();await show(currentView||'projects');});
389
+ $('wb-refresh').onclick=()=>busy($('wb-refresh'),()=>show(document.querySelector('[data-wb-view][aria-pressed="true"]')?.dataset.wbView||'overview'));
390
+
391
+ let hasToken = false;
392
+ try { hasToken = Boolean(sessionStorage.getItem('dodo-config-token')); } catch { hasToken = false; }
393
+ if (hasToken) void show('overview');
394
+ })();
@@ -0,0 +1,155 @@
1
+ import { Subagents } from '../services/ai/subagents.js';
2
+ import { openDatabase } from '../store/db.js';
3
+ import { Store } from '../store/store.js';
4
+ import { ProjectRegistry } from '../projects/registry.js';
5
+ import { DodoError } from '../errors.js';
6
+ import { bootstrapWorkspace } from './bootstrap.js';
7
+ import { attachOptionalServices } from './optionalServices.js';
8
+ import { projectAuthority, isOwner } from '../security/projectAuthority.js';
9
+ /** Explicit target runtimes never replace the legacy default workspace. */
10
+ export class InstallationRuntime {
11
+ initial;
12
+ current;
13
+ resourceFactory;
14
+ log;
15
+ runtimes = new Map();
16
+ opening = new Map();
17
+ closing = new Set();
18
+ resources = new Map();
19
+ users = new Map();
20
+ closed = false;
21
+ defaultReady = () => true;
22
+ registry;
23
+ store;
24
+ ai;
25
+ constructor(initial, current, resourceFactory, log, ports = [21730, 21731]) {
26
+ this.initial = initial;
27
+ this.current = current;
28
+ this.resourceFactory = resourceFactory;
29
+ this.log = log;
30
+ this.store = new Store(openDatabase(initial.paths.dbFile));
31
+ this.registry = new ProjectRegistry(this.store);
32
+ this.attach(initial);
33
+ this.ai = new Subagents(this, ports);
34
+ }
35
+ attach(ws) { ws.services.installation = this; }
36
+ list(p) {
37
+ return this.registry.list().filter(r => {
38
+ try {
39
+ return projectAuthority(this.store, p, r.workspaceId).scopes.includes('dodo:read');
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ });
45
+ }
46
+ project(id, p) {
47
+ const r = this.registry.list().find(r => r.projectId === id);
48
+ if (!r)
49
+ throw new DodoError('FORBIDDEN', 'project unavailable or not authorized');
50
+ const authority = projectAuthority(this.store, p, r.workspaceId);
51
+ if (authority.scopes.length === 0)
52
+ throw new DodoError('WORKSPACE_ACCESS_REQUIRED', 'owner must allow access to the target project');
53
+ if (!r.available)
54
+ throw new DodoError('PATH_DENIED', 'registered project directory is not ready');
55
+ return r;
56
+ }
57
+ async acquire(id, p) {
58
+ if (this.closed)
59
+ throw new DodoError('CONFLICT', 'runtime manager is closing');
60
+ if (this.closing.has(id))
61
+ throw new DodoError('CONFLICT', 'project runtime is closing; retry after it closes');
62
+ const r = this.project(id, p);
63
+ let ws = this.current();
64
+ if (ws.workspaceId === r.workspaceId && !this.defaultReady())
65
+ throw new DodoError('STALE_WORKSPACE', 'default project is changing; retry with fresh context');
66
+ if (ws.workspaceId !== r.workspaceId) {
67
+ const cached = this.runtimes.get(id);
68
+ if (cached)
69
+ ws = cached;
70
+ else {
71
+ let pending = this.opening.get(id);
72
+ if (!pending) {
73
+ if (this.runtimes.size + this.opening.size >= 16)
74
+ throw new DodoError('RESOURCE_LIMIT', 'close an idle project before opening another runtime');
75
+ pending = this.open(r.projectId, r.root);
76
+ this.opening.set(id, pending);
77
+ }
78
+ try {
79
+ ws = await pending;
80
+ }
81
+ finally {
82
+ this.opening.delete(id);
83
+ }
84
+ }
85
+ }
86
+ this.project(id, p); // identity/ACL may have changed while preparing resources
87
+ if (this.closing.has(id))
88
+ throw new DodoError('CONFLICT', 'project runtime is closing');
89
+ if (this.closed)
90
+ throw new DodoError('CONFLICT', 'runtime manager is closing');
91
+ this.users.set(ws.workspaceId, (this.users.get(ws.workspaceId) ?? 0) + 1);
92
+ let released = false;
93
+ return { workspace: ws, services: ws.services, release: () => {
94
+ if (released)
95
+ return;
96
+ released = true;
97
+ this.users.set(ws.workspaceId, (this.users.get(ws.workspaceId) ?? 1) - 1);
98
+ } };
99
+ }
100
+ async open(id, root) {
101
+ const ws = bootstrapWorkspace({ invokedCwd: root, configDir: { dir: this.initial.configDir, source: this.initial.configDirSource }, log: this.log });
102
+ try {
103
+ attachOptionalServices(ws, this.log);
104
+ this.attach(ws);
105
+ ws.services.listService.tree('.', { depth: 1, maxEntries: 5 });
106
+ this.resources.set(id, await this.resourceFactory(ws));
107
+ this.runtimes.set(id, ws);
108
+ ws.services.schedules.start(() => !this.closed);
109
+ return ws;
110
+ }
111
+ catch (err) {
112
+ await ws.shutdownServices();
113
+ throw err;
114
+ }
115
+ }
116
+ busy(workspaceId) {
117
+ if ((this.users.get(workspaceId) ?? 0) > 0)
118
+ return true;
119
+ return !this.closed && Boolean(this.store.db.prepare("SELECT 1 FROM ai_runs WHERE workspace_id=? AND status NOT IN ('completed','failed','canceled') LIMIT 1").get(workspaceId));
120
+ }
121
+ status() { return [...this.runtimes].map(([projectId, w]) => ({ projectId, state: this.closing.has(projectId) ? 'closing' : 'ready', workspaceId: w.workspaceId, workspaceEpoch: w.epoch, jobs: w.services.jobs.runningCount(), requests: this.users.get(w.workspaceId) ?? 0 })); }
122
+ async closeProject(id) {
123
+ if (this.opening.has(id))
124
+ throw new DodoError('CONFLICT', 'project is opening');
125
+ if (this.closing.has(id))
126
+ throw new DodoError('CONFLICT', 'project is closing');
127
+ const ws = this.runtimes.get(id);
128
+ if (!ws)
129
+ return false;
130
+ if (this.busy(ws.workspaceId) || ws.services.jobs.runningCount() || ws.services.mutations?.busy)
131
+ throw new DodoError('CONFLICT', 'project has active requests, jobs or agents');
132
+ this.closing.add(id);
133
+ await this.resources.get(id)?.close();
134
+ await ws.shutdownServices();
135
+ this.resources.delete(id);
136
+ this.runtimes.delete(id);
137
+ this.closing.delete(id);
138
+ return true;
139
+ }
140
+ async close() {
141
+ this.closed = true;
142
+ this.ai.stop();
143
+ await Promise.allSettled(this.opening.values());
144
+ await Promise.all([this.current(), ...this.runtimes.values()].map(w => w.services.jobs.shutdown(3000)));
145
+ await this.ai.close();
146
+ while ([...this.users.values()].some(n => n > 0))
147
+ await new Promise(r => setTimeout(r, 25));
148
+ for (const id of this.runtimes.keys())
149
+ await this.closeProject(id);
150
+ this.store.db.close();
151
+ }
152
+ owner() { return { grantId: 'local-config-owner', clientId: 'local-config', sub: 'owner', scopes: ['dodo:read', 'dodo:write', 'dodo:exec'] }; }
153
+ canManage(p) { return isOwner(p); }
154
+ }
155
+ //# sourceMappingURL=installationRuntime.js.map