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,1393 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * DODO CLI entrypoint. The FIRST statement evaluated in this module captures
4
+ * process.cwd() (spec §4.2) — ES module imports above it are hoisted but none
5
+ * of them changes the working directory at load time (enforced by test).
6
+ */
7
+ const invokedCwd = process.cwd();
8
+ import fs from 'node:fs';
9
+ import { Command } from 'commander';
10
+ import { startServer, DODO_VERSION } from '../server/appServer.js';
11
+ import { startStdioServer } from '../server/stdioServer.js';
12
+ import { resolveConfigDir, ensureConfigDir, statePaths, ipcSocketPath } from '../config/paths.js';
13
+ import { loadGlobalConfig, saveGlobalConfig, validatePublicUrl, GlobalConfigSchema } from '../config/globalConfig.js';
14
+ import { resolveWorkspaceRoot } from '../workspace/root.js';
15
+ import { mintWorkspaceId } from '../workspace/identity.js';
16
+ import { openDatabase, openDatabaseReadonly } from '../store/db.js';
17
+ import { Store } from '../store/store.js';
18
+ import { ipcCall, IpcError } from '../ipc/client.js';
19
+ import { installationIpcCall } from '../ipc/installationClient.js';
20
+ import { formatTerminalLine } from './terminal.js';
21
+ import { TRUST_MODE_DESCRIPTIONS } from '../security/policy.js';
22
+ import { DodoError } from '../errors.js';
23
+ import { sandboxAvailability } from '../services/jobs/sandbox.js';
24
+ import { DesktopPolicyInputSchema, saveDesktopPolicy } from '../services/desktop/desktopPolicy.js';
25
+ import { INPUT_LIMIT_PROFILES } from '../config/limits.js';
26
+ import { ProjectRegistry } from '../projects/registry.js';
27
+ import { runCliMenu } from './menu.js';
28
+ import { TunnelRuntime } from '../tunnel/runtime.js';
29
+ import { INSTALLATION_AUTHORITY_ID } from '../auth/constants.js';
30
+ const program = new Command();
31
+ program.name('dodo').description('DODO — local-first, single-owner, project-scoped coding MCP server').version(DODO_VERSION);
32
+ function fail(message, code = 1) {
33
+ console.error(`dodo: ${message}`);
34
+ process.exit(code);
35
+ }
36
+ function workspaceIpcPath(root) {
37
+ const { dir } = resolveConfigDir(process.env);
38
+ ensureConfigDir(dir);
39
+ const paths = statePaths(dir);
40
+ const rootInfo = resolveWorkspaceRoot(root ?? invokedCwd, { allowUnsafe: true });
41
+ // Read-only, no migration: never contend with a running server's write lock.
42
+ const db = openDatabaseReadonly(paths.dbFile);
43
+ if (!db) {
44
+ throw new IpcError('no running DODO server for this workspace (start it with `dodo start`)');
45
+ }
46
+ try {
47
+ const store = new Store(db);
48
+ const secret = store.readInstallSecret();
49
+ if (!secret) {
50
+ throw new IpcError('no running DODO server for this workspace (start it with `dodo start`)');
51
+ }
52
+ const wsId = mintWorkspaceId(secret, rootInfo.root);
53
+ return { ipcPath: ipcSocketPath(dir, wsId), root: rootInfo.root };
54
+ }
55
+ finally {
56
+ db.close();
57
+ }
58
+ }
59
+ async function ipcForCwd(cmd, args = {}) {
60
+ const { ipcPath } = workspaceIpcPath();
61
+ return ipcCall(ipcPath, cmd, args);
62
+ }
63
+ /** OAuth identity belongs to the installation, not the shell's CWD. */
64
+ async function ipcForInstallation(cmd, args = {}) {
65
+ const { dir } = resolveConfigDir(process.env);
66
+ return installationIpcCall(dir, cmd, args);
67
+ }
68
+ function printJsonOrLines(json, data, lines) {
69
+ if (json)
70
+ console.log(JSON.stringify(data, null, 2));
71
+ else
72
+ for (const l of lines())
73
+ console.log(l);
74
+ }
75
+ function withProjectRegistry(fn) {
76
+ const { dir } = resolveConfigDir(process.env);
77
+ ensureConfigDir(dir);
78
+ const db = openDatabase(statePaths(dir).dbFile);
79
+ try {
80
+ const store = new Store(db);
81
+ return fn(new ProjectRegistry(store), store);
82
+ }
83
+ finally {
84
+ db.close();
85
+ }
86
+ }
87
+ function projectLines(project) {
88
+ return [
89
+ `${project.displayName} ${project.projectId}`,
90
+ ` Path: ${project.root}`,
91
+ ` Workspace: ${project.workspaceId}`,
92
+ ` Status: ${project.availability} — ${project.statusText}`,
93
+ ` Updated: ${new Date(project.updatedAt).toISOString()}`,
94
+ ...(project.removedAt === null ? [] : [` Removed: ${new Date(project.removedAt).toISOString()}`]),
95
+ ];
96
+ }
97
+ function startupProject() {
98
+ try {
99
+ return withProjectRegistry((registry) => {
100
+ const configFile = statePaths(resolveConfigDir(process.env).dir).configFile;
101
+ const projectId = loadGlobalConfig(configFile).startupProjectId;
102
+ return projectId ? registry.get(projectId) : undefined;
103
+ });
104
+ }
105
+ catch {
106
+ return undefined;
107
+ }
108
+ }
109
+ function selectStartupProject(projectId) {
110
+ return withProjectRegistry((registry) => {
111
+ const project = registry.get(projectId);
112
+ if (!project.available)
113
+ throw new DodoError('CONFLICT', project.statusText);
114
+ const configFile = statePaths(resolveConfigDir(process.env).dir).configFile;
115
+ const config = loadGlobalConfig(configFile);
116
+ saveGlobalConfig(configFile, GlobalConfigSchema.parse({ ...config, startupProjectId: project.projectId }));
117
+ return project;
118
+ });
119
+ }
120
+ function addAndSelectProject(projectPath, displayName) {
121
+ return withProjectRegistry((registry) => {
122
+ const project = registry.add(projectPath, displayName).project;
123
+ const configFile = statePaths(resolveConfigDir(process.env).dir).configFile;
124
+ const config = loadGlobalConfig(configFile);
125
+ saveGlobalConfig(configFile, GlobalConfigSchema.parse({ ...config, startupProjectId: project.projectId }));
126
+ return project;
127
+ });
128
+ }
129
+ async function launchHttp(opts) {
130
+ if (Boolean(opts.allow) !== Boolean(opts.all))
131
+ fail('use --allow and --all together');
132
+ if (opts.tools !== undefined && !['compact', 'full', 'hybrid'].includes(opts.tools))
133
+ fail('--tools must be compact, full or hybrid');
134
+ let root = opts.root;
135
+ if (root !== undefined && !opts.allowUnsafeRoot)
136
+ root = addAndSelectProject(root).root;
137
+ if (root === undefined) {
138
+ const remembered = startupProject();
139
+ if (remembered?.available)
140
+ root = remembered.root;
141
+ }
142
+ const configDir = resolveConfigDir(process.env).dir;
143
+ const configFile = statePaths(configDir).configFile;
144
+ const config = loadGlobalConfig(configFile);
145
+ const publicUrl = opts.publicUrl ?? config.publicUrl;
146
+ let temporaryTunnelToken;
147
+ const interactiveTerminal = process.stdin.isTTY === true && process.stdout.isTTY === true;
148
+ if (opts.tunnel !== false && config.tunnel.startWithDodo && publicUrl && interactiveTerminal) {
149
+ const { readTemporaryTunnelToken } = await import('../tunnel/credentials.js');
150
+ temporaryTunnelToken = await readTemporaryTunnelToken();
151
+ }
152
+ const startOpts = {
153
+ configPort: config.configPort,
154
+ ...(opts.bypass ? { runMode: 'bypass' } : opts.allow ? { runMode: 'allow-all' } : {}),
155
+ invokedCwd,
156
+ ...(root === undefined ? { deferWorkspace: true } : { rootOverride: root }),
157
+ allowUnsafeRoot: opts.allowUnsafeRoot ?? false,
158
+ quiet: opts.quiet ?? false,
159
+ onLog: (line) => console.log(formatTerminalLine(line)),
160
+ };
161
+ if (opts.port !== undefined && !Number.isNaN(opts.port))
162
+ startOpts.portOverride = opts.port;
163
+ if (opts.publicUrl !== undefined)
164
+ startOpts.publicUrlOverride = opts.publicUrl;
165
+ if (opts.tools !== undefined)
166
+ startOpts.toolSurface = opts.tools;
167
+ const tunnelRuntime = new TunnelRuntime(configDir, line => console.log(`[dodo:tunnel] ${line}`));
168
+ let shutdownPromise;
169
+ let suppressOnStopped = false;
170
+ const server = await startServer({
171
+ ...startOpts,
172
+ tunnelRuntime,
173
+ onStopped: () => {
174
+ if (!shutdownPromise && !suppressOnStopped)
175
+ void stop('server stop');
176
+ },
177
+ });
178
+ if (temporaryTunnelToken) {
179
+ try {
180
+ const transientConfig = GlobalConfigSchema.parse({
181
+ ...config,
182
+ ...(publicUrl ? { publicUrl } : {}),
183
+ tunnel: { ...config.tunnel, mode: 'managed' },
184
+ });
185
+ const status = await tunnelRuntime.start(transientConfig, temporaryTunnelToken);
186
+ console.log(`[dodo] Cloudflare Tunnel attached for this run: ${status.publicOrigin}/mcp`);
187
+ }
188
+ catch (error) {
189
+ suppressOnStopped = true;
190
+ await server.close();
191
+ throw error;
192
+ }
193
+ finally {
194
+ temporaryTunnelToken = undefined;
195
+ }
196
+ }
197
+ else if (!opts.quiet && opts.tunnel !== false && config.tunnel.startWithDodo) {
198
+ if (!publicUrl)
199
+ console.log('[dodo] Tunnel skipped: configure the public HTTPS origin, then restart DODO.');
200
+ else if (!interactiveTerminal)
201
+ console.log('[dodo] Tunnel skipped: temporary token entry requires an interactive terminal.');
202
+ else
203
+ console.log('[dodo] Tunnel skipped for this run; local MCP remains available.');
204
+ }
205
+ async function stop(signal) {
206
+ if (shutdownPromise)
207
+ return shutdownPromise;
208
+ shutdownPromise = (async () => {
209
+ console.log(`\n[dodo] ${signal} received — shutting down (finishing journal safe point, closing jobs)…`);
210
+ await Promise.allSettled([server.close(), tunnelRuntime.close()]);
211
+ process.exit(0);
212
+ })();
213
+ return shutdownPromise;
214
+ }
215
+ process.on('SIGINT', () => void stop('SIGINT'));
216
+ process.on('SIGTERM', () => void stop('SIGTERM'));
217
+ }
218
+ async function setupFromMenu(check) {
219
+ const { runSetup, parseComponents } = await import('../setup/setup.js');
220
+ const configResolution = resolveConfigDir(process.env);
221
+ const report = await runSetup({
222
+ cwd: invokedCwd,
223
+ configDir: configResolution.dir,
224
+ check,
225
+ yes: !check,
226
+ components: parseComponents('all'),
227
+ detectExistingState: configResolution.source === 'platform',
228
+ }, line => console.log(line));
229
+ console.log(`DODO setup: ${report.platform}/${report.arch} (${report.mode})`);
230
+ for (const item of report.components)
231
+ console.log(`[${item.state}] ${item.component}: ${item.detail}${item.action ? ` — ${item.action}` : ''}`);
232
+ console.log(report.complete ? 'ทุก component พร้อมใช้งาน' : 'บาง component ยังต้องติดตั้ง อนุญาต หรือเตรียม backend เพิ่ม');
233
+ }
234
+ // Local-owner dependency setup. Never exposed as an MCP permission-changing tool.
235
+ program.command('setup')
236
+ .description('check and install missing local dependencies for this OS; existing security permissions remain in force')
237
+ .option('--check', 'read-only readiness report; no installation or configuration writes', false)
238
+ .option('--plan', 'read-only installation/readiness plan', false)
239
+ .option('--import-state', 'import only reviewed non-authority preferences from detected Dodo state; preserves the source', false)
240
+ .option('--yes', 'acknowledge reviewed package installations; never bypasses OS elevation or desktop consent', false)
241
+ .option('--components <list>', 'all, or comma-separated component names', 'all')
242
+ .option('--enable-web', 'explicitly permit SSRF-guarded outbound web access in owner config', false)
243
+ .option('--json', 'machine-readable report; progress goes to stderr', false)
244
+ .action(async (opts) => {
245
+ const { runSetup, parseComponents } = await import('../setup/setup.js');
246
+ try {
247
+ const configResolution = resolveConfigDir(process.env);
248
+ const report = await runSetup({ cwd: invokedCwd, configDir: configResolution.dir, check: opts.check, plan: opts.plan, yes: opts.yes, enableWeb: opts.enableWeb, components: parseComponents(opts.components), detectExistingState: configResolution.source === 'platform', importState: opts.importState }, line => opts.json ? console.error(line) : console.log(line));
249
+ if (opts.json)
250
+ console.log(JSON.stringify(report, null, 2));
251
+ else {
252
+ console.log(`DODO setup: ${report.platform}/${report.arch} (${report.mode})`);
253
+ for (const item of report.components)
254
+ console.log(`[${item.state}] ${item.component}: ${item.detail}${item.action ? ` — ${item.action}` : ''}`);
255
+ if (report.stateImport?.result) {
256
+ console.log(`Imported reviewed Dodo preferences: ${report.stateImport.result.importedConfigFields.join(', ') || 'none'} (source preserved)\nSecurity state reset: ${report.stateImport.result.resetSecurity.join('; ')}\nReceipt: ${report.stateImport.result.receipt}`);
257
+ }
258
+ else if (report.stateImport?.plan.state === 'available') {
259
+ console.log(`Existing Dodo config detected at ${report.stateImport.plan.sourceDir}. Importable preferences: ${report.stateImport.plan.importedConfigFields.join(', ') || 'none'}. OAuth, ACL, trust and permission state will not be copied. Re-run with --import-state to continue.`);
260
+ }
261
+ else if (report.stateImport?.plan.state === 'blocked') {
262
+ console.log(`Dodo state import requires local review: ${report.stateImport.plan.reason}`);
263
+ }
264
+ console.log(report.complete ? 'All selected components passed their readiness checks.' : 'Not all selected components are ready. Missing permissions/backends are not installation successes.');
265
+ if (report.receipt)
266
+ console.log(`Receipt: ${report.receipt}\nRestart existing DODO processes to load installed paths/LSP/config. OAuth grants and workspace access were not reset.`);
267
+ }
268
+ process.exitCode = report.exitCode;
269
+ }
270
+ catch (error) {
271
+ if (opts.json)
272
+ console.log(JSON.stringify({ error: error instanceof DodoError ? error.code : 'SETUP_FAILED', message: error.message }));
273
+ else
274
+ console.error(`dodo setup: ${error.message}`);
275
+ process.exitCode = 1;
276
+ }
277
+ });
278
+ // --------------------------------------------------------------- project --
279
+ // Installation-level owner registry. It exposes no MCP tool and grants no
280
+ // trust/client access; those remain keyed by the referenced workspace ID.
281
+ const project = program.command('project').description('manage the local owner-only project registry');
282
+ project.command('add <path>')
283
+ .description('register an absolute project directory without changing trust or client access')
284
+ .option('--name <display-name>', 'local display name (defaults to the directory name)')
285
+ .option('--json', 'machine-readable result', false)
286
+ .action((projectPath, opts) => {
287
+ try {
288
+ const result = withProjectRegistry((registry) => registry.add(projectPath, opts.name));
289
+ printJsonOrLines(opts.json, result, () => [
290
+ result.relocated ? 'Updated the location of an existing project identity.' : result.changed ? 'Project registered.' : 'Project was already registered; nothing changed.',
291
+ ...projectLines(result.project),
292
+ 'Trust, OAuth grants and workspace client access were not changed.',
293
+ ]);
294
+ }
295
+ catch (error) {
296
+ if (error instanceof DodoError)
297
+ fail(`${error.code}: ${error.message}${error.recovery ? `\n → ${error.recovery}` : ''}`);
298
+ throw error;
299
+ }
300
+ });
301
+ project.command('list')
302
+ .description('list registered projects and current path readiness')
303
+ .option('--all', 'include reviewed removals', false)
304
+ .option('--json', 'machine-readable output', false)
305
+ .action((opts) => {
306
+ try {
307
+ const projects = withProjectRegistry((registry) => registry.list({ includeRemoved: opts.all }));
308
+ printJsonOrLines(opts.json, projects, () => projects.length === 0
309
+ ? ['no projects registered (add one with: dodo project add /absolute/path)']
310
+ : projects.flatMap((entry, index) => [...(index ? [''] : []), ...projectLines(entry)]));
311
+ }
312
+ catch (error) {
313
+ if (error instanceof DodoError)
314
+ fail(`${error.code}: ${error.message}`);
315
+ throw error;
316
+ }
317
+ });
318
+ project.command('info <projectId>')
319
+ .description('show one project registry entry without displaying credentials')
320
+ .option('--json', 'machine-readable output', false)
321
+ .action((projectId, opts) => {
322
+ try {
323
+ const entry = withProjectRegistry((registry) => registry.get(projectId, { includeRemoved: true }));
324
+ printJsonOrLines(opts.json, entry, () => projectLines(entry));
325
+ }
326
+ catch (error) {
327
+ if (error instanceof DodoError)
328
+ fail(`${error.code}: ${error.message}`);
329
+ throw error;
330
+ }
331
+ });
332
+ project.command('remove <projectId>')
333
+ .description('remove one registry entry; never deletes project files, history, trust or ACL state')
334
+ .option('--yes', 'confirm the reviewed registry removal', false)
335
+ .option('--json', 'machine-readable output', false)
336
+ .action((projectId, opts) => {
337
+ try {
338
+ const result = withProjectRegistry((registry) => {
339
+ const current = registry.get(projectId);
340
+ if (!opts.yes) {
341
+ throw new DodoError('APPROVAL_REQUIRED', `removing ${current.displayName} (${current.projectId}) requires --yes`, {
342
+ recovery: `review ${current.root}, then run: dodo project remove ${current.projectId} --yes`,
343
+ });
344
+ }
345
+ return registry.remove(projectId);
346
+ });
347
+ printJsonOrLines(opts.json, { removed: true, project: result, filesDeleted: false, authorityDeleted: false }, () => [
348
+ `Removed ${result.displayName} (${result.projectId}) from the project registry.`,
349
+ 'Project files, workspace history, trust and client ACL state were preserved.',
350
+ ]);
351
+ }
352
+ catch (error) {
353
+ if (error instanceof DodoError)
354
+ fail(`${error.code}: ${error.message}${error.recovery ? `\n → ${error.recovery}` : ''}`);
355
+ throw error;
356
+ }
357
+ });
358
+ // --------------------------------------------------------------- tunnel ----
359
+ const tunnel = program.command('tunnel').description('manage or inspect a Cloudflare remotely-managed Tunnel without managing DNS or the Cloudflare account');
360
+ tunnel.command('configure')
361
+ .description('save local tunnel mode and a credential reference; tokens are never written to config')
362
+ .option('--managed', 'DODO supervises cloudflared in the foreground', false)
363
+ .option('--external', 'an owner/system service manages the tunnel', false)
364
+ .option('--os-credential', 'prompt through the reviewed OS credential provider', false)
365
+ .option('--token-env <name>', 'reference an uppercase environment variable that already contains the token')
366
+ .option('--token-file <absolute-path>', 'reference an owner-private absolute token file')
367
+ .option('--cloudflared <absolute-path>', 'owner-selected cloudflared executable outside any workspace')
368
+ .option('--metrics-port <n>', 'loopback cloudflared readiness port', (v) => Number(v))
369
+ .option('--max-restarts <n>', 'bounded supervisor restarts, 0..5', (v) => Number(v))
370
+ .option('--remove-credential', 'delete an OS-stored credential and clear the reference (requires --yes)', false)
371
+ .option('--yes', 'confirm credential deletion; does not start cloudflared', false)
372
+ .option('--json', 'machine-readable non-secret result', false)
373
+ .action(async (opts) => {
374
+ try {
375
+ if (opts.managed && opts.external)
376
+ fail('choose --managed or --external');
377
+ const methods = [opts.osCredential, opts.tokenEnv !== undefined, opts.tokenFile !== undefined].filter(Boolean).length;
378
+ if (methods > 1)
379
+ fail('choose only one of --os-credential, --token-env or --token-file');
380
+ if (opts.removeCredential && methods > 0)
381
+ fail('--remove-credential cannot be combined with a credential source');
382
+ if (opts.removeCredential && !opts.yes)
383
+ fail('credential deletion requires --yes');
384
+ const { dir } = resolveConfigDir(process.env);
385
+ ensureConfigDir(dir);
386
+ const paths = statePaths(dir), current = loadGlobalConfig(paths.configFile);
387
+ const credentials = await import('../tunnel/credentials.js');
388
+ const { resolveCloudflared } = await import('../tunnel/supervisor.js');
389
+ const previousCredentialRef = current.tunnel.credentialRef;
390
+ let credentialRef = current.tunnel.credentialRef;
391
+ if (opts.removeCredential)
392
+ credentialRef = undefined;
393
+ const baseTunnelInput = {
394
+ ...current.tunnel,
395
+ mode: opts.managed ? 'managed' : opts.external ? 'external' : current.tunnel.mode,
396
+ ...(credentialRef ? { credentialRef } : {}),
397
+ ...(opts.metricsPort !== undefined ? { metricsPort: opts.metricsPort } : {}),
398
+ ...(opts.maxRestarts !== undefined ? { maxRestarts: opts.maxRestarts } : {}),
399
+ ...(opts.cloudflared !== undefined ? { executable: opts.cloudflared } : {}),
400
+ };
401
+ if (!credentialRef)
402
+ delete baseTunnelInput['credentialRef'];
403
+ let next = GlobalConfigSchema.parse({ ...current, tunnel: baseTunnelInput });
404
+ if (opts.cloudflared !== undefined) {
405
+ const canonical = resolveCloudflared(next);
406
+ next = GlobalConfigSchema.parse({ ...next, tunnel: { ...next.tunnel, executable: canonical } });
407
+ }
408
+ if (opts.osCredential) {
409
+ const ref = credentials.osTunnelCredentialRef(dir);
410
+ await credentials.storeOsTunnelCredentialInteractive(ref);
411
+ credentialRef = ref;
412
+ }
413
+ else if (opts.tokenEnv !== undefined)
414
+ credentialRef = credentials.envTunnelCredentialRef(opts.tokenEnv);
415
+ else if (opts.tokenFile !== undefined)
416
+ credentialRef = credentials.fileTunnelCredentialRef(opts.tokenFile);
417
+ const withCredential = { ...next.tunnel, ...(credentialRef ? { credentialRef } : {}) };
418
+ if (!credentialRef)
419
+ delete withCredential['credentialRef'];
420
+ next = GlobalConfigSchema.parse({ ...next, tunnel: withCredential });
421
+ if (next.tunnel.mode === 'managed' && !next.tunnel.credentialRef)
422
+ fail('managed mode requires --os-credential, --token-env or --token-file');
423
+ saveGlobalConfig(paths.configFile, next);
424
+ if (opts.removeCredential && previousCredentialRef)
425
+ credentials.deleteOsTunnelCredential(previousCredentialRef);
426
+ const result = { mode: next.tunnel.mode, credential: next.tunnel.credentialRef ? 'configured' : 'not-configured', metricsPort: next.tunnel.metricsPort, maxRestarts: next.tunnel.maxRestarts, cloudflared: next.tunnel.executable ? 'owner-selected' : 'trusted-PATH', started: false };
427
+ if (opts.json)
428
+ console.log(JSON.stringify(result, null, 2));
429
+ else {
430
+ console.log(`Tunnel mode: ${result.mode}`);
431
+ console.log(`Credential: ${result.credential} (value is not stored in config or displayed)`);
432
+ console.log(`Readiness: 127.0.0.1:${result.metricsPort} Restarts: ${result.maxRestarts}`);
433
+ console.log(result.mode === 'managed' ? 'Configuration saved. Start explicitly with: dodo tunnel start --yes' : 'External mode saved. DODO will observe health but will not start cloudflared.');
434
+ }
435
+ }
436
+ catch (error) {
437
+ if (error instanceof DodoError)
438
+ fail(`${error.code}: ${error.message}${error.recovery ? `\n → ${error.recovery}` : ''}`);
439
+ throw error;
440
+ }
441
+ });
442
+ async function runTunnelForeground(restart, json) {
443
+ const { dir } = resolveConfigDir(process.env);
444
+ ensureConfigDir(dir);
445
+ const config = loadGlobalConfig(statePaths(dir).configFile);
446
+ const control = await import('../tunnel/control.js');
447
+ if (restart) {
448
+ try {
449
+ await control.stopTunnel(dir);
450
+ }
451
+ catch (error) {
452
+ if (!(error instanceof IpcError))
453
+ throw error;
454
+ }
455
+ const deadline = Date.now() + 10_000;
456
+ while ((await control.tunnelStatus(dir, config)).supervisor && Date.now() < deadline)
457
+ await new Promise(resolve => setTimeout(resolve, 100));
458
+ if ((await control.tunnelStatus(dir, config)).supervisor)
459
+ throw new DodoError('TIMEOUT', 'existing tunnel supervisor did not stop within 10 seconds');
460
+ }
461
+ const { startManagedTunnel } = await import('../tunnel/supervisor.js');
462
+ const supervisor = await startManagedTunnel({ configDir: dir, config, onLog: line => { if (!json)
463
+ console.log(`[dodo:tunnel] ${line}`); } });
464
+ if (json)
465
+ console.log(JSON.stringify({ started: true, status: supervisor.status() }));
466
+ else
467
+ console.log(`Tunnel supervisor is running in the foreground for ${supervisor.status().publicOrigin}. Press Ctrl-C to stop.`);
468
+ const stop = () => supervisor.stop();
469
+ process.once('SIGINT', stop);
470
+ process.once('SIGTERM', stop);
471
+ if (process.platform !== 'win32')
472
+ process.once('SIGHUP', stop);
473
+ const exitCode = await supervisor.wait();
474
+ process.removeListener('SIGINT', stop);
475
+ process.removeListener('SIGTERM', stop);
476
+ if (process.platform !== 'win32')
477
+ process.removeListener('SIGHUP', stop);
478
+ if (exitCode !== 0)
479
+ process.exitCode = exitCode;
480
+ }
481
+ for (const name of ['start', 'restart']) {
482
+ tunnel.command(name)
483
+ .description(name === 'start' ? 'start the configured managed tunnel in the foreground' : 'stop the current managed supervisor and start it again in the foreground')
484
+ .option('--yes', 'confirm starting a process that connects this machine to the configured Cloudflare Tunnel', false)
485
+ .option('--json', 'machine-readable startup/status line; diagnostics remain private', false)
486
+ .action(async (opts) => {
487
+ if (!opts.yes)
488
+ fail(`dodo tunnel ${name} requires --yes; no process was started`);
489
+ try {
490
+ await runTunnelForeground(name === 'restart', opts.json);
491
+ }
492
+ catch (error) {
493
+ if (error instanceof DodoError || error instanceof IpcError)
494
+ fail(`${error instanceof DodoError ? `${error.code}: ` : ''}${error.message}`);
495
+ throw error;
496
+ }
497
+ });
498
+ }
499
+ tunnel.command('stop').description('ask the authenticated local managed supervisor to stop its owned cloudflared process')
500
+ .option('--json', 'machine-readable result', false)
501
+ .action(async (opts) => {
502
+ try {
503
+ const { stopTunnel } = await import('../tunnel/control.js');
504
+ const result = await stopTunnel(resolveConfigDir(process.env).dir);
505
+ if (opts.json)
506
+ console.log(JSON.stringify(result));
507
+ else
508
+ console.log('Stop requested. DODO will not signal any saved PID or unrelated process.');
509
+ }
510
+ catch (error) {
511
+ if (error instanceof IpcError)
512
+ fail('no authenticated managed tunnel supervisor is running', 2);
513
+ throw error;
514
+ }
515
+ });
516
+ tunnel.command('status').description('show configured mode and authenticated supervisor evidence without reading the credential')
517
+ .option('--json', 'machine-readable report', false)
518
+ .action(async (opts) => {
519
+ const { dir } = resolveConfigDir(process.env), config = loadGlobalConfig(statePaths(dir).configFile);
520
+ const { tunnelStatus } = await import('../tunnel/control.js');
521
+ const report = await tunnelStatus(dir, config);
522
+ if (opts.json)
523
+ console.log(JSON.stringify(report, null, 2));
524
+ else {
525
+ console.log(`Configured mode: ${report.configuredMode}`);
526
+ if (report.supervisor)
527
+ console.log(`Supervisor: ${report.supervisor.phase}; connected=${report.supervisor.connected}; restarts=${report.supervisor.restarts}/${report.supervisor.maxRestarts}`);
528
+ else
529
+ console.log(`Supervisor: not running${report.lastKnown ? `; last phase=${report.lastKnown.phase} at ${report.lastKnown.updatedAt}` : ''}`);
530
+ }
531
+ });
532
+ tunnel.command('doctor').description('explicitly probe cloudflared, credential availability, local MCP health and configured public health')
533
+ .option('--json', 'machine-readable non-secret report', false)
534
+ .action(async (opts) => {
535
+ const { dir } = resolveConfigDir(process.env), config = loadGlobalConfig(statePaths(dir).configFile);
536
+ const { tunnelDoctor } = await import('../tunnel/control.js');
537
+ const report = await tunnelDoctor(dir, config);
538
+ console.log(opts.json ? JSON.stringify(report, null, 2) : JSON.stringify(report, null, 2));
539
+ });
540
+ tunnel.command('logs').description('show bounded redacted managed-tunnel diagnostics')
541
+ .option('--lines <n>', 'last 1..500 lines', (v) => Number(v), 200)
542
+ .option('--json', 'machine-readable result', false)
543
+ .action(async (opts) => {
544
+ if (!Number.isSafeInteger(opts.lines) || opts.lines < 1 || opts.lines > 500)
545
+ fail('--lines must be an integer from 1 to 500');
546
+ const { tunnelLogs } = await import('../tunnel/control.js');
547
+ const result = await tunnelLogs(resolveConfigDir(process.env).dir, opts.lines);
548
+ if (opts.json)
549
+ console.log(JSON.stringify(result, null, 2));
550
+ else
551
+ for (const line of result.lines)
552
+ console.log(line);
553
+ });
554
+ // --------------------------------------------------------------- menu -----
555
+ program.command('cli')
556
+ .description('open the interactive local owner menu')
557
+ .action(async () => {
558
+ try {
559
+ await runCliMenu({
560
+ listProjects: () => withProjectRegistry((registry) => registry.list()),
561
+ startupProject,
562
+ selectProject: selectStartupProject,
563
+ addProject: addAndSelectProject,
564
+ start: async (root, tunnel = true) => launchHttp({ ...(root === undefined ? {} : { root }), tunnel }),
565
+ setupAll: () => setupFromMenu(false),
566
+ checkSetup: () => setupFromMenu(true),
567
+ }, { input: process.stdin, output: process.stdout });
568
+ }
569
+ catch (error) {
570
+ if (error instanceof DodoError)
571
+ fail(`${error.code}: ${error.message}${error.recovery ? `\n → ${error.recovery}` : ''}`);
572
+ throw error;
573
+ }
574
+ });
575
+ // ---------------------------------------------------------------- start ----
576
+ program
577
+ .command('start')
578
+ .description('start the MCP server for the saved project, or wait for a project selection (foreground)')
579
+ .option('--root <path>', 'select and remember an explicit workspace root (local CLI only)')
580
+ .option('--port <n>', 'listen port (default from config, initially 21730)', (v) => Number.parseInt(v, 10))
581
+ .option('--public-url <url>', 'public origin override for this run (local CLI only)')
582
+ .option('--allow-unsafe-root', 'permit filesystem root / home / other unusually broad workspace roots', false)
583
+ .option('--allow', 'allow task execution for this run (requires --all)', false)
584
+ .option('--all', 'all local action classes (requires --allow)', false)
585
+ .option('--bypass', 'trusted actions and no default job sandbox for this run; OAuth and file guards remain', false)
586
+ .option('--tools <surface>', 'tool exposure for this run: compact (HTTP default), full, or hybrid (49 tools: gateways + common direct tools); permissions are unchanged')
587
+ .option('--no-tunnel', 'start local MCP only; do not ask for a temporary Cloudflare Tunnel token')
588
+ .option('--quiet', 'suppress the startup banner', false)
589
+ .action(async (opts) => {
590
+ try {
591
+ await launchHttp(opts);
592
+ }
593
+ catch (err) {
594
+ if (err instanceof DodoError)
595
+ fail(`${err.message}${err.recovery ? `\n → ${err.recovery}` : ''}`);
596
+ throw err;
597
+ }
598
+ });
599
+ const schedule = program.command('schedule').alias('cron').description('immutable scheduled commands; separate local approval required');
600
+ schedule.command('create').requiredOption('--name <name>', 'human-readable task name')
601
+ .requiredOption('--command <command>', 'exact shell command, max 64 KiB')
602
+ .requiredOption('--cron <expression>', 'five-field cron (minute hour day month weekday)')
603
+ .option('--timezone <zone>', 'IANA time zone', 'UTC')
604
+ .requiredOption('--expires <ISO>', 'consent expiry, ISO date-time within 30 days')
605
+ .option('--cwd <path>', 'workspace-relative working directory', '.')
606
+ .option('--timeout <ms>', 'maximum runtime in milliseconds', '300000')
607
+ .option('--no-sandbox', 'explicitly run with OS user privileges without an OS sandbox')
608
+ .option('--network', 'allow network in the requested sandbox', false)
609
+ .action(async (o) => {
610
+ console.log(JSON.stringify(await ipcForCwd('schedule.propose', { name: o.name, command: o.command, cron: o.cron, timezone: o.timezone, expiresAt: Date.parse(o.expires), cwd: o.cwd, timeoutMs: Number(o.timeout), sandbox: o.sandbox, network: o.network }), null, 2));
611
+ console.log('PENDING: inspect with dodo schedule show ID, then approve the exact digest. Runs current project code; not pinned to a revision.');
612
+ });
613
+ schedule.command('list').action(async () => console.log(JSON.stringify(await ipcForCwd('schedule.list'), null, 2)));
614
+ schedule.command('show <id>').action(async (id) => console.log(JSON.stringify(await ipcForCwd('schedule.show', { id }), null, 2)));
615
+ schedule.command('approve <id>').requiredOption('--digest <sha256>', 'exact digest from the reviewed schedule')
616
+ .action(async (id, o) => console.log(JSON.stringify(await ipcForCwd('schedule.approve', { id, digest: o.digest }), null, 2)));
617
+ schedule.command('revoke <id>').action(async (id) => console.log(JSON.stringify(await ipcForCwd('schedule.revoke', { id }), null, 2)));
618
+ schedule.command('history <id>').action(async (id) => console.log(JSON.stringify(await ipcForCwd('schedule.history', { id }), null, 2)));
619
+ // --------------------------------------------------------------- memory --
620
+ const memory = program.command('memory').description('review evidence-backed project memory and reusable-learning proposals over private owner IPC');
621
+ memory.command('pending')
622
+ .description('list pending memory proposals for this active workspace')
623
+ .action(async () => console.log(JSON.stringify(await ipcForCwd('memory.pending'), null, 2)));
624
+ memory.command('show <id>')
625
+ .description('inspect one exact memory proposal or approved memory, including its review digest')
626
+ .action(async (id) => console.log(JSON.stringify(await ipcForCwd('memory.show', { id }), null, 2)));
627
+ memory.command('list')
628
+ .description('list owner-approved memory visible to this active workspace')
629
+ .option('--include-stale', 'include stale/expired/source-changed records', false)
630
+ .action(async (opts) => console.log(JSON.stringify(await ipcForCwd('memory.list', { includeStale: opts.includeStale }), null, 2)));
631
+ memory.command('approve <id>')
632
+ .description('approve the exact reviewed proposal; never changes source files, permissions or executable policy')
633
+ .requiredOption('--digest <sha256>', 'exact digest shown by dodo memory show')
634
+ .option('--share-with <projectIds...>', 'owner-authorized registered project IDs that may read this memory', [])
635
+ .option('--allow-conflict', 'explicitly retain this proposal alongside listed conflicting current memories', false)
636
+ .action(async (id, opts) => {
637
+ console.log(JSON.stringify(await ipcForCwd('memory.approve', { id, digest: opts.digest, shareWith: opts.shareWith, allowConflict: opts.allowConflict }), null, 2));
638
+ });
639
+ memory.command('reject <id>')
640
+ .description('reject a pending memory proposal')
641
+ .option('--note <text>', 'short non-secret owner review note', '')
642
+ .action(async (id, opts) => console.log(JSON.stringify(await ipcForCwd('memory.reject', { id, note: opts.note }), null, 2)));
643
+ memory.command('reverify <id>')
644
+ .description('restore a stale memory only when every original evidence hash matches again')
645
+ .requiredOption('--digest <sha256>', 'exact memory contentHash from dodo memory show')
646
+ .action(async (id, opts) => console.log(JSON.stringify(await ipcForCwd('memory.reverify', { id, digest: opts.digest }), null, 2)));
647
+ memory.command('prune')
648
+ .description('delete old rejected/expired proposals and stale memories; CURRENT memory is never pruned')
649
+ .requiredOption('--older-than <days>', 'minimum age in days', (value) => Number.parseInt(value, 10))
650
+ .option('--yes', 'confirm reviewed deletion', false)
651
+ .action(async (opts) => {
652
+ if (!opts.yes)
653
+ fail('memory prune requires --yes after reviewing dodo memory list --include-stale');
654
+ console.log(JSON.stringify(await ipcForCwd('memory.prune', { olderThanDays: opts.olderThan }), null, 2));
655
+ });
656
+ const memoryLearning = memory.command('learning').description('review workflow/skill suggestions; approval never installs or executes them');
657
+ memoryLearning.command('pending').action(async () => console.log(JSON.stringify(await ipcForCwd('memory.learning.pending'), null, 2)));
658
+ memoryLearning.command('show <id>').action(async (id) => console.log(JSON.stringify(await ipcForCwd('memory.learning.show', { id }), null, 2)));
659
+ memoryLearning.command('approve <id>')
660
+ .requiredOption('--digest <sha256>', 'exact digest shown by dodo memory learning show')
661
+ .option('--note <text>', 'short non-secret owner review note', '')
662
+ .action(async (id, opts) => console.log(JSON.stringify(await ipcForCwd('memory.learning.review', { id, digest: opts.digest, note: opts.note, approved: true }), null, 2)));
663
+ memoryLearning.command('reject <id>')
664
+ .requiredOption('--digest <sha256>', 'exact digest shown by dodo memory learning show')
665
+ .option('--note <text>', 'short non-secret owner review note', '')
666
+ .action(async (id, opts) => console.log(JSON.stringify(await ipcForCwd('memory.learning.review', { id, digest: opts.digest, note: opts.note, approved: false }), null, 2)));
667
+ // ----------------------------------------------------- advanced agent --
668
+ const agent = program.command('agent').description('owner review for reusable Advanced Agent Runtime skills');
669
+ const agentSkill = agent.command('skill').description('review versioned skill guidance over private owner IPC');
670
+ agentSkill.command('pending')
671
+ .description('list pending skill proposals for this active workspace')
672
+ .action(async () => console.log(JSON.stringify(await ipcForCwd('agent.skill.pending'), null, 2)));
673
+ agentSkill.command('show <id>')
674
+ .description('inspect one exact proposal/skill and its digest')
675
+ .action(async (id) => console.log(JSON.stringify(await ipcForCwd('agent.skill.show', { id }), null, 2)));
676
+ agentSkill.command('approve <id>')
677
+ .description('approve reviewed guidance as the next immutable skill version; never installs or executes it')
678
+ .requiredOption('--digest <sha256>', 'exact digest shown by dodo agent skill show')
679
+ .option('--note <text>', 'short non-secret owner review note', '')
680
+ .action(async (id, opts) => console.log(JSON.stringify(await ipcForCwd('agent.skill.review', { id, digest: opts.digest, note: opts.note, approved: true }), null, 2)));
681
+ agentSkill.command('reject <id>')
682
+ .description('reject a pending untrusted skill proposal')
683
+ .requiredOption('--digest <sha256>', 'exact digest shown by dodo agent skill show')
684
+ .option('--note <text>', 'short non-secret owner review note', '')
685
+ .action(async (id, opts) => console.log(JSON.stringify(await ipcForCwd('agent.skill.review', { id, digest: opts.digest, note: opts.note, approved: false }), null, 2)));
686
+ // ----------------------------------------------------------------- init ----
687
+ program
688
+ .command('init')
689
+ .description('set the global public URL profile (does not modify the repository)')
690
+ .requiredOption('--public-url <url>', 'public HTTPS origin of your tunnel, e.g. https://dodo.example.com')
691
+ .option('--port <n>', 'default listen port', (v) => Number.parseInt(v, 10))
692
+ .option('--dangerously-allow-insecure-http', 'allow an http:// public URL (LOCAL TESTS ONLY)', false)
693
+ .action((opts) => {
694
+ try {
695
+ const { dir } = resolveConfigDir(process.env);
696
+ ensureConfigDir(dir);
697
+ const paths = statePaths(dir);
698
+ const config = loadGlobalConfig(paths.configFile);
699
+ const next = { ...config, dangerouslyAllowInsecurePublicUrl: opts.dangerouslyAllowInsecureHttp || config.dangerouslyAllowInsecurePublicUrl };
700
+ const url = validatePublicUrl(opts.publicUrl, next.dangerouslyAllowInsecurePublicUrl);
701
+ next.publicUrl = url.origin;
702
+ if (opts.port !== undefined && !Number.isNaN(opts.port))
703
+ next.port = opts.port;
704
+ saveGlobalConfig(paths.configFile, GlobalConfigSchema.parse(next));
705
+ console.log(`Saved global config: ${paths.configFile}`);
706
+ console.log(`Public URL: ${url.origin} Port: ${next.port}`);
707
+ console.log('');
708
+ console.log('Next steps:');
709
+ console.log(` 1. Point your own tunnel at 127.0.0.1:${next.port} for host ${url.hostname} (see docs/TUNNEL.md — route ALL paths, not just /mcp)`);
710
+ console.log(' 2. Register your client callback: dodo auth add-client --redirect-uri <exact callback URL from your client UI>');
711
+ console.log(' 3. cd into a project, set trust: dodo trust --mode edit');
712
+ console.log(' 4. dodo start');
713
+ }
714
+ catch (err) {
715
+ if (err instanceof DodoError)
716
+ fail(err.message);
717
+ throw err;
718
+ }
719
+ });
720
+ // ---------------------------------------------------------------- limits ---
721
+ program.command('limits')
722
+ .description('show input budgets or save a local profile (keeps OAuth, trust and output limits)')
723
+ .option('--profile <name>', 'save standard or large input budgets; restart DODO to apply')
724
+ .option('--json', 'machine-readable output', false)
725
+ .action((opts) => {
726
+ if (opts.profile !== undefined && opts.profile !== 'standard' && opts.profile !== 'large')
727
+ fail('profile must be standard or large (unlimited is not supported)');
728
+ const { dir } = resolveConfigDir(process.env);
729
+ const { configFile } = statePaths(dir);
730
+ const config = loadGlobalConfig(configFile);
731
+ if (opts.profile !== undefined) {
732
+ ensureConfigDir(dir);
733
+ config.limits = { ...config.limits, ...INPUT_LIMIT_PROFILES[opts.profile] };
734
+ saveGlobalConfig(configFile, config);
735
+ }
736
+ const result = { configFile, limits: config.limits, restartRequired: opts.profile !== undefined };
737
+ printJsonOrLines(opts.json, result, () => [
738
+ `DODO input limits: ${configFile}`,
739
+ ` Shell command: ${config.limits.commandBytes} UTF-8 bytes`,
740
+ ` Text file: ${config.limits.readFileBytes} UTF-8 bytes`,
741
+ ` Change plan: ${config.limits.previewAggregateBytes} bytes of resulting content`,
742
+ ` MCP request: ${config.limits.requestBodyBytes} bytes including JSON framing/escaping`,
743
+ ' Native argv: 65536 bytes per argument; 131072 bytes combined',
744
+ ...(opts.profile !== undefined ? ['Saved. Restart DODO to apply. OAuth, trust, sandbox and output caps were preserved.'] : ['For larger inputs: dodo limits --profile large (then restart DODO)']),
745
+ ]);
746
+ });
747
+ // ---------------------------------------------------------------- doctor ---
748
+ program
749
+ .command('doctor')
750
+ .description('check the local environment (prints no secrets)')
751
+ .option('--json', 'machine-readable output', false)
752
+ .action(async (opts) => {
753
+ const checks = [];
754
+ const [nodeMajor = 0, nodeMinor = 0] = process.versions.node.split('.').map(Number);
755
+ const nodeOk = nodeMajor > 22 || (nodeMajor === 22 && nodeMinor >= 12);
756
+ checks.push({ name: 'node', ok: nodeOk, note: `v${process.versions.node} (need >=22.12)` });
757
+ const { dir } = resolveConfigDir(process.env);
758
+ let cfgOk = true;
759
+ let cfgNote = dir;
760
+ try {
761
+ ensureConfigDir(dir);
762
+ const { activateManagedTools } = await import('../setup/managedTools.js');
763
+ activateManagedTools(dir, invokedCwd);
764
+ const st = fs.statSync(dir);
765
+ if (process.platform !== 'win32' && (st.mode & 0o077) !== 0) {
766
+ cfgOk = false;
767
+ cfgNote = `${dir} has group/other permissions (expected 0700)`;
768
+ }
769
+ }
770
+ catch (err) {
771
+ cfgOk = false;
772
+ cfgNote = err.message;
773
+ }
774
+ checks.push({ name: 'config-dir', ok: cfgOk, note: cfgNote });
775
+ try {
776
+ const paths = statePaths(dir);
777
+ const db = openDatabase(paths.dbFile);
778
+ try {
779
+ checks.push({ name: 'state-db', ok: true, note: 'opens and migrates' });
780
+ const projects = new ProjectRegistry(new Store(db)).list();
781
+ const invalid = projects.filter((entry) => entry.availability === 'invalid').length;
782
+ const unavailable = projects.filter((entry) => !entry.available && entry.availability !== 'invalid').length;
783
+ checks.push({ name: 'projects', ok: invalid === 0, note: `${projects.length} registered; ${projects.length - invalid - unavailable} ready; ${unavailable} unavailable; ${invalid} invalid` });
784
+ }
785
+ finally {
786
+ db.close();
787
+ }
788
+ }
789
+ catch (err) {
790
+ checks.push({ name: 'state-db', ok: false, note: err.message });
791
+ checks.push({ name: 'projects', ok: false, note: 'registry unavailable because state database did not open' });
792
+ }
793
+ try {
794
+ const config = loadGlobalConfig(statePaths(dir).configFile);
795
+ checks.push({ name: 'public-url', ok: config.publicUrl !== undefined, note: config.publicUrl ?? 'not configured (server will start LOCKED)' });
796
+ }
797
+ catch (err) {
798
+ checks.push({ name: 'public-url', ok: false, note: err.message });
799
+ }
800
+ const { spawnSync } = await import('node:child_process');
801
+ const { resolveTrustedExecutable } = await import('../platform/execResolve.js');
802
+ const { buildChildEnv } = await import('../security/env.js');
803
+ const { shellSpec } = await import('../platform/shell.js');
804
+ const env = buildChildEnv({ parentEnv: process.env, workspaceRoot: invokedCwd, extraAllowlist: [] });
805
+ for (const [bin, name] of [['git', 'git'], ['rg', 'ripgrep']]) {
806
+ try {
807
+ const executable = resolveTrustedExecutable(bin, invokedCwd, { allowBatch: false });
808
+ const probe = spawnSync(executable, ['--version'], { cwd: invokedCwd, env, shell: false, windowsHide: true, timeout: 3000 });
809
+ checks.push({ name, ok: bin === 'rg' || probe.status === 0, note: probe.status === 0 ? String(probe.stdout).split('\n')[0].trim() : 'probe failed; no capability claimed' });
810
+ }
811
+ catch {
812
+ checks.push({ name, ok: bin === 'rg', note: bin === 'rg' ? 'not found; bounded JavaScript search is available' : 'not found on trusted PATH (repository executables are not probed)' });
813
+ }
814
+ }
815
+ try {
816
+ const shell = shellSpec(invokedCwd);
817
+ checks.push({ name: 'shell', ok: true, note: `${shell.kind}: ${shell.executable}` });
818
+ }
819
+ catch (error) {
820
+ checks.push({ name: 'shell', ok: false, note: error.message });
821
+ }
822
+ checks.push({ name: 'platform', ok: true, note: process.platform === 'win32' ? 'Windows native: authenticated named-pipe IPC and owned-tree cancellation; optional desktop, speech and sandbox backends are checked with dodo setup --check' : `${process.platform}/${process.arch}; authenticated Unix-socket IPC` });
823
+ const sb = sandboxAvailability();
824
+ checks.push({ name: 'sandbox', ok: true, note: sb.available ? `available (${sb.kind})` : `unavailable — ${sb.reason ?? 'unsupported platform'} (sandbox-required commands are refused; no automatic downgrade)` });
825
+ try {
826
+ const cfg = loadGlobalConfig(statePaths(dir).configFile);
827
+ const langs = Object.keys(cfg.lsp);
828
+ checks.push({ name: 'lsp', ok: true, note: langs.length === 0 ? 'none registered (TypeScript/JavaScript built in); dodo lsp add <lang> ...' : `registered: ${langs.join(', ')}` });
829
+ checks.push({ name: 'command-sandbox', ok: true, note: `commandSandbox=${cfg.commandSandbox}; allowWebFetch=${cfg.allowWebFetch}` });
830
+ }
831
+ catch {
832
+ /* config errors already reported above */
833
+ }
834
+ try {
835
+ const require2 = (await import('node:module')).createRequire(import.meta.url);
836
+ require2('better-sqlite3');
837
+ checks.push({ name: 'sqlite-native', ok: true, note: 'prebuilt binding loads' });
838
+ }
839
+ catch (err) {
840
+ checks.push({ name: 'sqlite-native', ok: false, note: err.message });
841
+ }
842
+ printJsonOrLines(opts.json, checks, () => checks.map((c) => `${c.ok ? 'ok ' : 'FAIL'} ${c.name.padEnd(14)} ${c.note}`));
843
+ if (checks.some((c) => !c.ok))
844
+ process.exitCode = 1;
845
+ });
846
+ // ---------------------------------------------------------------- status ---
847
+ program
848
+ .command('status')
849
+ .description('show the running server for this directory (via private IPC)')
850
+ .option('--json', 'machine-readable output', false)
851
+ .action(async (opts) => {
852
+ try {
853
+ const data = (await ipcForCwd('status'));
854
+ printJsonOrLines(opts.json, data, () => [
855
+ `DODO ${data.version} pid ${data.pid}`,
856
+ `Workspace: ${data.root}`,
857
+ `Workspace ID: ${data.workspaceId} epoch: ${data.workspaceEpoch}`,
858
+ `Port: ${data.port} ${data.locked ? 'LOCKED (no public URL)' : `Public: ${data.publicUrl}`}`,
859
+ `Trust mode: ${data.trustMode}`,
860
+ `Running jobs: ${data.runningJobs}`,
861
+ ...(data.recoveryRequired.length > 0 ? [`RECOVERY REQUIRED: ${data.recoveryRequired.join(', ')} (run: dodo recover)`] : []),
862
+ ]);
863
+ }
864
+ catch (err) {
865
+ if (err instanceof IpcError)
866
+ fail(err.message, 2);
867
+ throw err;
868
+ }
869
+ });
870
+ // ------------------------------------------------------------------ stop ---
871
+ program
872
+ .command('stop')
873
+ .description('gracefully stop the running server for this directory')
874
+ .action(async () => {
875
+ try {
876
+ await ipcForCwd('stop');
877
+ console.log('stopping');
878
+ }
879
+ catch (err) {
880
+ if (err instanceof IpcError)
881
+ fail(err.message, 2);
882
+ throw err;
883
+ }
884
+ });
885
+ // ------------------------------------------------------------------ kill ---
886
+ program
887
+ .command('kill')
888
+ .description('stop all DODO servers and their jobs for this user/config, from any folder; preserve OAuth login')
889
+ .option('--timeout <seconds>', 'shutdown wait per server (1..120 seconds)', Number, 30)
890
+ .option('--json', 'machine-readable shutdown report', false)
891
+ .action(async (opts) => {
892
+ if (!Number.isInteger(opts.timeout) || opts.timeout < 1 || opts.timeout > 120)
893
+ fail('--timeout must be an integer from 1 to 120 seconds');
894
+ const { killServers } = await import('./kill.js');
895
+ const { dir } = resolveConfigDir(process.env);
896
+ if (!opts.json)
897
+ console.log('Stopping DODO servers and their jobs; keeping OAuth login and configuration…');
898
+ const result = await killServers(dir, opts.timeout * 1000);
899
+ printJsonOrLines(opts.json, result, () => [
900
+ ...result.stopped.map(s => `Stopped DODO ${s.pid} (${s.transport}): ${s.root}`),
901
+ ...result.failed.map(s => `Could not stop ${s.root}: ${s.reason}`),
902
+ ...(result.stopped.length === 0 && result.failed.length === 0 ? ['No running DODO servers found.'] : []),
903
+ `State: ${dir}`,
904
+ 'OAuth clients, keys, tokens and saved permissions are kept. Start dodo again with the same public URL to use existing valid login credentials.',
905
+ ]);
906
+ if (result.failed.length)
907
+ process.exitCode = 1;
908
+ });
909
+ // ----------------------------------------------------------------- trust ---
910
+ program
911
+ .command('trust')
912
+ .description('set the local trust mode for THIS workspace directory')
913
+ .requiredOption('--mode <mode>', 'inspect | edit | trusted')
914
+ .option('--yes', 'confirm trusted mode non-interactively', false)
915
+ .action((opts) => {
916
+ const mode = opts.mode;
917
+ if (!['inspect', 'edit', 'trusted'].includes(mode))
918
+ fail('mode must be inspect, edit, or trusted');
919
+ if (mode === 'trusted' && !opts.yes) {
920
+ fail('trusted mode lets the remote AI run commands with YOUR user privileges (no OS sandbox; scripts can read/write outside this folder).\n' +
921
+ 'If you understand that, repeat with: dodo trust --mode trusted --yes');
922
+ }
923
+ try {
924
+ const { dir } = resolveConfigDir(process.env);
925
+ ensureConfigDir(dir);
926
+ const paths = statePaths(dir);
927
+ const rootInfo = resolveWorkspaceRoot(invokedCwd, {});
928
+ const db = openDatabase(paths.dbFile);
929
+ try {
930
+ const store = new Store(db);
931
+ const wsId = mintWorkspaceId(store.installSecret(), rootInfo.root);
932
+ store.upsertWorkspace({ id: wsId, root: rootInfo.root, dev: rootInfo.dev, ino: rootInfo.ino, epoch: 'cli' });
933
+ store.setTrustMode(wsId, mode);
934
+ }
935
+ finally {
936
+ db.close();
937
+ }
938
+ console.log(`Trust mode for ${rootInfo.root}: ${mode}`);
939
+ console.log(` ${TRUST_MODE_DESCRIPTIONS[mode]}`);
940
+ console.log('A running server picks this up immediately.');
941
+ }
942
+ catch (err) {
943
+ if (err instanceof DodoError)
944
+ fail(err.message);
945
+ throw err;
946
+ }
947
+ });
948
+ // --------------------------------------------------------------- approve ---
949
+ program
950
+ .command('approve <requestId>')
951
+ .description('approve a pending action request shown by an APPROVAL_REQUIRED error')
952
+ .action(async (requestId) => {
953
+ try {
954
+ const res = (await ipcForCwd('approvals.approve', { id: requestId }));
955
+ console.log(`approved: ${requestId}`);
956
+ console.log(` ${res.summary}`);
957
+ console.log('The remote client can now retry the SAME call (same arguments).');
958
+ }
959
+ catch (err) {
960
+ if (err instanceof IpcError)
961
+ fail(err.message, 2);
962
+ throw err;
963
+ }
964
+ });
965
+ program
966
+ .command('deny <requestId>')
967
+ .description('deny a pending action request')
968
+ .action(async (requestId) => {
969
+ try {
970
+ await ipcForCwd('approvals.deny', { id: requestId });
971
+ console.log(`denied: ${requestId}`);
972
+ }
973
+ catch (err) {
974
+ if (err instanceof IpcError)
975
+ fail(err.message, 2);
976
+ throw err;
977
+ }
978
+ });
979
+ program
980
+ .command('pending')
981
+ .description('list pending action approvals for this workspace')
982
+ .option('--json', 'machine-readable output', false)
983
+ .action(async (opts) => {
984
+ try {
985
+ const rows = (await ipcForCwd('approvals.pending'));
986
+ printJsonOrLines(opts.json, rows, () => rows.length === 0
987
+ ? ['no pending approvals']
988
+ : rows.map((r) => `${r.id} [${r.tool}] ${r.summary} (expires ${new Date(r.expiresAt).toLocaleTimeString()})`));
989
+ }
990
+ catch (err) {
991
+ if (err instanceof IpcError)
992
+ fail(err.message, 2);
993
+ throw err;
994
+ }
995
+ });
996
+ // ------------------------------------------------------------------ auth ---
997
+ const auth = program.command('auth').description('OAuth client registration, consent and grants');
998
+ auth
999
+ .command('add-client')
1000
+ .description('register a static OAuth client (copy the EXACT redirect URI from your client UI)')
1001
+ .requiredOption('--redirect-uri <uri...>', 'exact redirect/callback URI(s)')
1002
+ .option('--name <name>', 'display name', 'dodo remote client')
1003
+ .option('--public', 'public client (PKCE only, no secret)', false)
1004
+ .option('--json', 'machine-readable output', false)
1005
+ .action((opts) => {
1006
+ try {
1007
+ const { dir } = resolveConfigDir(process.env);
1008
+ ensureConfigDir(dir);
1009
+ const paths = statePaths(dir);
1010
+ const db = openDatabase(paths.dbFile);
1011
+ try {
1012
+ const store = new Store(db);
1013
+ const info = (function register() {
1014
+ const req = { redirectUris: opts.redirectUri, name: opts.name };
1015
+ if (opts.public)
1016
+ req.public = true;
1017
+ return addStaticClientLocal(store, req);
1018
+ })();
1019
+ if (opts.json) {
1020
+ console.log(JSON.stringify(info, null, 2));
1021
+ }
1022
+ else {
1023
+ console.log('Registered OAuth client (shown ONCE — store it in your client config now):');
1024
+ console.log(` client_id: ${info.clientId}`);
1025
+ if (info.clientSecret)
1026
+ console.log(` client_secret: ${info.clientSecret}`);
1027
+ console.log(` redirect URIs: ${info.redirectUris.join(', ')}`);
1028
+ console.log(` auth method: ${info.tokenEndpointAuthMethod}`);
1029
+ }
1030
+ }
1031
+ finally {
1032
+ db.close();
1033
+ }
1034
+ }
1035
+ catch (err) {
1036
+ if (err instanceof DodoError)
1037
+ fail(err.message);
1038
+ throw err;
1039
+ }
1040
+ });
1041
+ auth
1042
+ .command('clients')
1043
+ .description('list registered OAuth clients')
1044
+ .option('--json', 'machine-readable output', false)
1045
+ .action((opts) => {
1046
+ const { dir } = resolveConfigDir(process.env);
1047
+ ensureConfigDir(dir);
1048
+ const db = openDatabase(statePaths(dir).dbFile);
1049
+ try {
1050
+ const store = new Store(db);
1051
+ const rows = listStaticClientsLocal(store);
1052
+ printJsonOrLines(opts.json, rows, () => rows.length === 0 ? ['no clients registered'] : rows.map((c) => `${c.clientId} ${c.name} → ${c.redirectUris.join(', ')}`));
1053
+ }
1054
+ finally {
1055
+ db.close();
1056
+ }
1057
+ });
1058
+ auth
1059
+ .command('pending')
1060
+ .description('list pending OAuth authorization requests (verify id + phrase against the browser page)')
1061
+ .option('--json', 'machine-readable output', false)
1062
+ .action(async (opts) => {
1063
+ try {
1064
+ const rows = (await ipcForInstallation('auth.pending'));
1065
+ printJsonOrLines(opts.json, rows, () => rows.length === 0
1066
+ ? ['no pending authorization requests']
1067
+ : rows.flatMap((r) => [
1068
+ `id: ${r.id}`,
1069
+ `phrase: ${r.phrase} ← must match the browser page`,
1070
+ `client: ${r.clientId}`,
1071
+ `callback: ${r.redirectUri}`,
1072
+ `scopes: ${r.scopes}`,
1073
+ r.accessMode === 'personal'
1074
+ ? 'access: registered projects, bounded by these OAuth scopes (personal mode)'
1075
+ : 'access: installation login only; assign project scopes later in Local Config (managed mode)',
1076
+ `approve: dodo auth approve -- ${r.id}`,
1077
+ '',
1078
+ ]));
1079
+ }
1080
+ catch (err) {
1081
+ if (err instanceof IpcError)
1082
+ fail(err.message, 2);
1083
+ throw err;
1084
+ }
1085
+ });
1086
+ auth
1087
+ .command('approve <id>')
1088
+ .description('approve a pending OAuth authorization request')
1089
+ .action(async (id) => {
1090
+ try {
1091
+ await ipcForInstallation('auth.approve', { id });
1092
+ console.log(`approved: ${id}`);
1093
+ console.log('The browser page will now finish the sign-in automatically.');
1094
+ }
1095
+ catch (err) {
1096
+ if (err instanceof IpcError)
1097
+ fail(err.message, 2);
1098
+ throw err;
1099
+ }
1100
+ });
1101
+ auth
1102
+ .command('deny <id>')
1103
+ .description('deny a pending OAuth authorization request')
1104
+ .action(async (id) => {
1105
+ try {
1106
+ await ipcForInstallation('auth.deny', { id });
1107
+ console.log(`denied: ${id}`);
1108
+ }
1109
+ catch (err) {
1110
+ if (err instanceof IpcError)
1111
+ fail(err.message, 2);
1112
+ throw err;
1113
+ }
1114
+ });
1115
+ auth
1116
+ .command('list')
1117
+ .description('list grants (no tokens are shown)')
1118
+ .option('--json', 'machine-readable output', false)
1119
+ .action(async (opts) => {
1120
+ try {
1121
+ const rows = (await ipcForInstallation('auth.grants'));
1122
+ printJsonOrLines(opts.json, rows, () => rows.length === 0
1123
+ ? ['no grants']
1124
+ : rows.map((g) => `${g.grantId} client=${g.clientId} scopes=${g.scopes.join(',')} ${g.installationIdentity ? '(installation identity)' : g.thisWorkspace ? '(this workspace)' : '(legacy workspace identity)'}${g.revokedAt ? ' REVOKED' : ''}`));
1125
+ }
1126
+ catch (err) {
1127
+ if (err instanceof IpcError)
1128
+ fail(err.message, 2);
1129
+ throw err;
1130
+ }
1131
+ });
1132
+ auth
1133
+ .command('revoke <grantId>')
1134
+ .description('revoke a grant immediately (access + refresh tokens die with it)')
1135
+ .action(async (grantId) => {
1136
+ try {
1137
+ await ipcForInstallation('auth.revoke', { grantId });
1138
+ console.log(`revoked: ${grantId}`);
1139
+ }
1140
+ catch (err) {
1141
+ if (err instanceof IpcError)
1142
+ fail(err.message, 2);
1143
+ throw err;
1144
+ }
1145
+ });
1146
+ // --------------------------------------------------------------- recover ---
1147
+ program
1148
+ .command('recover')
1149
+ .description('list changesets needing manual recovery; resolve with --resolve <id> after fixing files')
1150
+ .option('--resolve <changesetId>', 'mark a changeset resolved after manual fixes')
1151
+ .option('--json', 'machine-readable output', false)
1152
+ .action(async (opts) => {
1153
+ try {
1154
+ if (opts.resolve !== undefined) {
1155
+ await ipcForCwd('recover.resolve', { changesetId: opts.resolve, action: 'mark-resolved' });
1156
+ console.log(`marked resolved: ${opts.resolve}`);
1157
+ return;
1158
+ }
1159
+ const rows = (await ipcForCwd('recover.list'));
1160
+ printJsonOrLines(opts.json, rows, () => rows.length === 0
1161
+ ? ['nothing needs recovery']
1162
+ : rows.flatMap((r) => [`${r.changesetId} ${r.summary ?? ''}`, ` problem: ${r.error ?? 'unknown'}`, ` backups: <config-dir>/backups/${r.changesetId}/`, '']));
1163
+ }
1164
+ catch (err) {
1165
+ if (err instanceof IpcError)
1166
+ fail(err.message, 2);
1167
+ throw err;
1168
+ }
1169
+ });
1170
+ // ----------------------------------------------------------------- stdio ---
1171
+ program
1172
+ .command('stdio')
1173
+ .description('serve MCP over stdin/stdout for a LOCAL client (Claude Code, Cursor, Codex CLI, Claude Desktop) — no tunnel or OAuth')
1174
+ .option('--root <path>', 'workspace root (defaults to the current directory)')
1175
+ .option('--allow-unsafe-root', 'permit filesystem root / home / other unusually broad workspace roots', false)
1176
+ .option('--tools <surface>', 'tool exposure for this run: full (STDIO default), compact, or hybrid; permissions are unchanged')
1177
+ .action(async (opts) => {
1178
+ try {
1179
+ if (opts.tools !== undefined && !['compact', 'full', 'hybrid'].includes(opts.tools))
1180
+ fail('--tools must be compact, full or hybrid');
1181
+ const startOpts = { invokedCwd, allowUnsafeRoot: opts.allowUnsafeRoot, onStopped: () => process.exit(0) };
1182
+ if (opts.root !== undefined)
1183
+ startOpts.rootOverride = opts.root;
1184
+ if (opts.tools !== undefined)
1185
+ startOpts.toolSurface = opts.tools;
1186
+ const server = await startStdioServer(startOpts);
1187
+ const stop = async () => {
1188
+ await server.close();
1189
+ process.exit(0);
1190
+ };
1191
+ process.stdin.on('end', () => void stop());
1192
+ process.stdin.on('close', () => void stop());
1193
+ process.on('SIGINT', () => void stop());
1194
+ process.on('SIGTERM', () => void stop());
1195
+ }
1196
+ catch (err) {
1197
+ if (err instanceof DodoError)
1198
+ fail(`${err.message}${err.recovery ? `\n → ${err.recovery}` : ''}`);
1199
+ throw err;
1200
+ }
1201
+ });
1202
+ // ----------------------------------------------------------------- audit ---
1203
+ program
1204
+ .command('audit')
1205
+ .description('show what the AI did in this workspace (recent tool calls; no file contents or secrets)')
1206
+ .option('--limit <n>', 'number of entries', (v) => Number.parseInt(v, 10), 50)
1207
+ .option('--json', 'machine-readable output', false)
1208
+ .action((opts) => {
1209
+ const { dir } = resolveConfigDir(process.env);
1210
+ const paths = statePaths(dir);
1211
+ const db = openDatabaseReadonly(paths.dbFile);
1212
+ if (!db)
1213
+ fail('no DODO state yet for this machine (nothing has run)', 2);
1214
+ try {
1215
+ const store = new Store(db);
1216
+ const secret = store.readInstallSecret();
1217
+ const rootInfo = resolveWorkspaceRoot(invokedCwd, { allowUnsafe: true });
1218
+ const wsId = secret ? mintWorkspaceId(secret, rootInfo.root) : undefined;
1219
+ const rows = store.recentAudit(wsId, Number.isNaN(opts.limit) ? 50 : opts.limit);
1220
+ printJsonOrLines(opts.json, rows, () => rows.length === 0
1221
+ ? ['no audit entries for this workspace']
1222
+ : rows.map((r) => `${new Date(r.ts).toISOString()} ${(r.tool ?? '-').padEnd(18)} ${r.result.padEnd(22)} ${r.durationMs ?? 0}ms ${r.principal ?? ''}`));
1223
+ }
1224
+ finally {
1225
+ db.close();
1226
+ }
1227
+ });
1228
+ // ------------------------------------------------------------------- lsp ---
1229
+ const lsp = program.command('lsp').description('owner-installed language servers for symbols/references/rename in non-TS languages');
1230
+ lsp
1231
+ .command('add <language>')
1232
+ .description('register a language server, e.g. dodo lsp add python --command pyright-langserver --args --stdio --ext .py --ext .pyi')
1233
+ .requiredOption('--command <cmd>', 'server executable (bare name on your PATH, or absolute path outside the workspace)')
1234
+ .option('--args <args...>', 'arguments', [])
1235
+ .requiredOption('--ext <ext...>', 'file extensions handled, e.g. .py')
1236
+ .action((language, opts) => {
1237
+ try {
1238
+ const { dir } = resolveConfigDir(process.env);
1239
+ ensureConfigDir(dir);
1240
+ const paths = statePaths(dir);
1241
+ const config = loadGlobalConfig(paths.configFile);
1242
+ const next = GlobalConfigSchema.parse({ ...config, lsp: { ...config.lsp, [language]: { command: opts.command, args: opts.args, extensions: opts.ext } } });
1243
+ saveGlobalConfig(paths.configFile, next);
1244
+ console.log(`Registered LSP for ${language}: ${opts.command} ${opts.args.join(' ')} (${opts.ext.join(', ')})`);
1245
+ console.log('The server is started on first use and runs with YOUR user privileges. Restart dodo to pick this up.');
1246
+ }
1247
+ catch (err) {
1248
+ if (err instanceof DodoError)
1249
+ fail(err.message);
1250
+ throw err;
1251
+ }
1252
+ });
1253
+ lsp
1254
+ .command('list')
1255
+ .description('list registered language servers')
1256
+ .option('--json', 'machine-readable output', false)
1257
+ .action((opts) => {
1258
+ const { dir } = resolveConfigDir(process.env);
1259
+ const config = loadGlobalConfig(statePaths(dir).configFile);
1260
+ const rows = Object.entries(config.lsp).map(([language, c]) => ({ language, ...c }));
1261
+ printJsonOrLines(opts.json, rows, () => (rows.length === 0 ? ['no language servers registered (TypeScript/JavaScript are built in)'] : rows.map((r) => `${r.language.padEnd(10)} ${r.command} ${r.args.join(' ')} [${r.extensions.join(', ')}]`)));
1262
+ });
1263
+ lsp
1264
+ .command('remove <language>')
1265
+ .description('unregister a language server')
1266
+ .action((language) => {
1267
+ const { dir } = resolveConfigDir(process.env);
1268
+ const paths = statePaths(dir);
1269
+ const config = loadGlobalConfig(paths.configFile);
1270
+ const rest = { ...config.lsp };
1271
+ delete rest[language];
1272
+ saveGlobalConfig(paths.configFile, GlobalConfigSchema.parse({ ...config, lsp: rest }));
1273
+ console.log(`removed ${language}`);
1274
+ });
1275
+ // Only local owner commands can save desktop consent. No running server is
1276
+ // needed for persistent grants or revocation. Persistent native-app consent is
1277
+ // installation-wide; active services still bind snapshots/receipts to their
1278
+ // workspace and re-check OAuth, trust and current policy before every call.
1279
+ function saveInstallationDesktopPolicy(input) {
1280
+ const args = DesktopPolicyInputSchema.parse(input);
1281
+ const { dir } = resolveConfigDir(process.env);
1282
+ ensureConfigDir(dir);
1283
+ const db = openDatabase(statePaths(dir).dbFile);
1284
+ try {
1285
+ const store = new Store(db);
1286
+ // A CWD-independent owner command may be the first command used with a
1287
+ // fresh state directory. Initialize the installation identity just as a
1288
+ // normal server bootstrap would, without creating a workspace identity.
1289
+ store.installSecret();
1290
+ return db.transaction(() => saveDesktopPolicy(store, INSTALLATION_AUTHORITY_ID, 'local-cli', args, Date.now()))();
1291
+ }
1292
+ finally {
1293
+ db.close();
1294
+ }
1295
+ }
1296
+ const desktop = program.command('desktop').description('native window capture/control setup and local app permission');
1297
+ desktop.command('apps').description('local owner only: list Windows/Linux executable identities to choose exact app grants')
1298
+ .action(async () => {
1299
+ if (!['win32', 'linux'].includes(process.platform))
1300
+ fail('On macOS use application bundle IDs; this command lists Windows/Linux executable identities.');
1301
+ const { NativeDesktopBackend } = await import('../services/desktop/nativeBackend.js');
1302
+ console.log(JSON.stringify(await new NativeDesktopBackend(resolveConfigDir(process.env).dir).run({ op: 'apps' }), null, 2));
1303
+ });
1304
+ desktop.command('setup')
1305
+ .description('prepare the platform helper (macOS Swift, Windows .NET, Linux X11/AT-SPI)')
1306
+ .option('--request-permissions', 'request/check platform desktop permission where the OS exposes an owner prompt', false)
1307
+ .action(async (opts) => {
1308
+ const { setupNativeDesktop } = await import('../services/desktop/nativeBackend.js');
1309
+ const { dir } = resolveConfigDir(process.env);
1310
+ ensureConfigDir(dir);
1311
+ const result = await setupNativeDesktop(dir, opts.requestPermissions);
1312
+ console.log(JSON.stringify(result, null, 2));
1313
+ console.log('OS setup does not change DODO access. To allow a named app once for this installation: dodo desktop allow --app <app-id> --mode control --persist --yes');
1314
+ console.log(process.platform === 'win32'
1315
+ ? 'Windows: use dodo desktop apps for exact IDs. An unlocked interactive session is required; no UAC/secure desktop bypass.'
1316
+ : process.platform === 'linux'
1317
+ ? 'Linux: use dodo desktop apps for exact IDs. X11/XWayland with DISPLAY, XTEST/X-Resource and AT-SPI is required; pure Wayland portal-only sessions are not claimed.'
1318
+ : 'macOS permissions: System Settings > Privacy & Security > Screen & System Audio Recording / Accessibility. Restart DODO after changing OS permissions if needed.');
1319
+ });
1320
+ desktop.command('status').description('show desktop permission and helper status for this running workspace')
1321
+ .action(async () => console.log(JSON.stringify(await ipcForCwd('desktop.status'), null, 2)));
1322
+ desktop.command('allow')
1323
+ .description('permit named apps; --persist remembers consent once for this DODO installation')
1324
+ .requiredOption('--app <bundleIds...>', 'exact macOS bundle IDs or win.<hash>/linux.<hash> IDs from dodo desktop apps')
1325
+ .option('--mode <mode>', 'view | control', 'view')
1326
+ .option('--minutes <n>', 'temporary permission lifetime 1..480 minutes (default 60); excludes --persist', (v) => Number(v))
1327
+ .option('--persist', 'remember this app grant for the DODO installation until disabled', false)
1328
+ .option('--yes', 'acknowledge that screen/UI access can expose data and affect apps outside the workspace', false)
1329
+ .action(async (opts) => {
1330
+ if (!['view', 'control'].includes(opts.mode))
1331
+ fail('desktop mode must be view or control');
1332
+ if (!opts.yes)
1333
+ fail('Desktop access can expose private screen/UI data outside this workspace; control can change apps with your OS-user rights. Repeat with --yes to authorize the named apps.');
1334
+ if (opts.persist && opts.minutes !== undefined)
1335
+ fail('use --persist or --minutes, not both');
1336
+ if (process.platform === 'win32' || process.platform === 'linux') {
1337
+ const { NativeDesktopBackend } = await import('../services/desktop/nativeBackend.js');
1338
+ const backend = new NativeDesktopBackend(resolveConfigDir(process.env).dir);
1339
+ if (!backend.available())
1340
+ fail('NOT_SUPPORTED: run dodo setup --components desktop locally first; no desktop permission was saved.');
1341
+ const pattern = process.platform === 'win32' ? /^win\.[a-f0-9]{40}$/ : /^linux\.[a-f0-9]{40}$/;
1342
+ if (opts.app.some(app => !pattern.test(app)))
1343
+ fail('Use exact platform app IDs from dodo desktop apps, not executable basenames.');
1344
+ }
1345
+ const input = { mode: opts.mode, allowedApps: opts.app, persistent: opts.persist, ...(opts.minutes !== undefined ? { minutes: opts.minutes } : {}) };
1346
+ if (opts.persist) {
1347
+ const saved = saveInstallationDesktopPolicy(input);
1348
+ console.log(JSON.stringify(saved, null, 2));
1349
+ console.log('Remembered until disabled for this DODO installation. Every project reuses this native-app consent.');
1350
+ }
1351
+ else {
1352
+ console.log(JSON.stringify(await ipcForCwd('desktop.policy', input), null, 2));
1353
+ console.log('Temporary grant: resets on restart/workspace switch. Use --persist instead of --minutes to remember it.');
1354
+ }
1355
+ console.log('Revoke and forget: dodo desktop disable (or the private Local Config stop button).');
1356
+ });
1357
+ desktop.command('disable').description('revoke and forget installation-wide desktop consent, even while the server is stopped')
1358
+ .action(() => {
1359
+ const saved = saveInstallationDesktopPolicy({ mode: 'off' });
1360
+ console.log(JSON.stringify(saved, null, 2));
1361
+ console.log('Desktop access disabled and forgotten for this DODO installation.');
1362
+ });
1363
+ // Static-client helpers are imported lazily to keep CLI startup light.
1364
+ import { addStaticClient as addStaticClientLocal, listStaticClients as listStaticClientsLocal } from '../auth/clients.js';
1365
+ program.addHelpText('after', `
1366
+ Local HTTP + private config:
1367
+ dodo start the saved project; config on 127.0.0.1:21731
1368
+ dodo kill stop DODO across folders, keep OAuth login
1369
+ dodo limits --profile large larger command/file budgets; restart to apply
1370
+ dodo start --allow --all trusted tasks for this run
1371
+ dodo --bypass trusted tasks, default command sandbox off
1372
+ MCP subprocess clients:
1373
+ dodo stdio --root /path/to/project explicit local STDIO transport
1374
+ Desktop (platform helper and explicit app permission required):
1375
+ dodo desktop setup prepare the platform desktop backend
1376
+ dodo desktop disable revoke desktop access
1377
+ Remote setup:
1378
+ dodo init --public-url https://... configure public origin once
1379
+ dodo start prompt for a temporary Tunnel token
1380
+ dodo start --no-tunnel run local MCP only`);
1381
+ function effectiveArgv() {
1382
+ if (process.argv.length <= 2)
1383
+ return [...process.argv, 'start'];
1384
+ if (process.argv.length === 3 && process.argv[2] === '--cli')
1385
+ return [...process.argv.slice(0, 2), 'cli'];
1386
+ if (process.argv[2] === '--bypass')
1387
+ return [...process.argv.slice(0, 2), 'start', ...process.argv.slice(2)];
1388
+ return process.argv;
1389
+ }
1390
+ program.parseAsync(effectiveArgv()).catch((err) => {
1391
+ fail(err instanceof Error ? err.message : String(err));
1392
+ });
1393
+ //# sourceMappingURL=main.js.map