nikcli 0.0.6

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 (602) hide show
  1. package/.turbo/turbo-typecheck.log +1 -0
  2. package/AGENTS.md +27 -0
  3. package/Dockerfile +18 -0
  4. package/README.md +15 -0
  5. package/bin/nikcli +84 -0
  6. package/config.json +13 -0
  7. package/docs/tailscale-mobile/01-tailscale-setup.md +94 -0
  8. package/docs/tailscale-mobile/02-host-setup.md +115 -0
  9. package/docs/tailscale-mobile/03-phone-and-serve.md +134 -0
  10. package/docs/tailscale-mobile/README.md +59 -0
  11. package/examples/README.md +54 -0
  12. package/package.json +147 -0
  13. package/parsers-config.ts +253 -0
  14. package/script/build.ts +179 -0
  15. package/script/postinstall.mjs +125 -0
  16. package/script/publish-registries.ts +187 -0
  17. package/script/publish.ts +100 -0
  18. package/script/schema.ts +47 -0
  19. package/script/seed-e2e.ts +50 -0
  20. package/sequential-prancing-forest.md +373 -0
  21. package/src/acp/README.md +164 -0
  22. package/src/acp/agent.ts +1303 -0
  23. package/src/acp/session.ts +105 -0
  24. package/src/acp/types.ts +22 -0
  25. package/src/agent/agent.ts +528 -0
  26. package/src/agent/generate.txt +32 -0
  27. package/src/agent/prompt/compaction.txt +14 -0
  28. package/src/agent/prompt/explore.txt +18 -0
  29. package/src/agent/prompt/summary.txt +11 -0
  30. package/src/agent/prompt/title.txt +44 -0
  31. package/src/auth/index.ts +73 -0
  32. package/src/bun/index.ts +119 -0
  33. package/src/bun/registry.ts +54 -0
  34. package/src/bus/bus-event.ts +43 -0
  35. package/src/bus/global.ts +10 -0
  36. package/src/bus/index.ts +105 -0
  37. package/src/chatbot/handlers.ts +150 -0
  38. package/src/chatbot/index.ts +132 -0
  39. package/src/cli/bootstrap.ts +17 -0
  40. package/src/cli/cmd/acp.ts +69 -0
  41. package/src/cli/cmd/ads.ts +377 -0
  42. package/src/cli/cmd/agent.ts +259 -0
  43. package/src/cli/cmd/auth.ts +400 -0
  44. package/src/cli/cmd/chatbot.ts +420 -0
  45. package/src/cli/cmd/cmd.ts +7 -0
  46. package/src/cli/cmd/companion.ts +81 -0
  47. package/src/cli/cmd/connectors.ts +593 -0
  48. package/src/cli/cmd/debug/agent.ts +166 -0
  49. package/src/cli/cmd/debug/config.ts +16 -0
  50. package/src/cli/cmd/debug/file.ts +97 -0
  51. package/src/cli/cmd/debug/index.ts +48 -0
  52. package/src/cli/cmd/debug/lsp.ts +52 -0
  53. package/src/cli/cmd/debug/ripgrep.ts +87 -0
  54. package/src/cli/cmd/debug/scrap.ts +16 -0
  55. package/src/cli/cmd/debug/skill.ts +16 -0
  56. package/src/cli/cmd/debug/snapshot.ts +52 -0
  57. package/src/cli/cmd/export.ts +88 -0
  58. package/src/cli/cmd/generate.ts +38 -0
  59. package/src/cli/cmd/github.ts +412 -0
  60. package/src/cli/cmd/image-model.ts +128 -0
  61. package/src/cli/cmd/import.ts +201 -0
  62. package/src/cli/cmd/lovable.ts +128 -0
  63. package/src/cli/cmd/mcp.ts +738 -0
  64. package/src/cli/cmd/mobile.ts +223 -0
  65. package/src/cli/cmd/models.ts +77 -0
  66. package/src/cli/cmd/plug.ts +231 -0
  67. package/src/cli/cmd/pr.ts +104 -0
  68. package/src/cli/cmd/rag-model.ts +167 -0
  69. package/src/cli/cmd/remote.ts +416 -0
  70. package/src/cli/cmd/run.ts +589 -0
  71. package/src/cli/cmd/serve.ts +51 -0
  72. package/src/cli/cmd/session.ts +133 -0
  73. package/src/cli/cmd/speak-model.ts +204 -0
  74. package/src/cli/cmd/stats.ts +402 -0
  75. package/src/cli/cmd/tui/app.tsx +841 -0
  76. package/src/cli/cmd/tui/attach.ts +31 -0
  77. package/src/cli/cmd/tui/component/border.tsx +75 -0
  78. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  79. package/src/cli/cmd/tui/component/dialog-command.tsx +172 -0
  80. package/src/cli/cmd/tui/component/dialog-config.tsx +291 -0
  81. package/src/cli/cmd/tui/component/dialog-connectors.tsx +440 -0
  82. package/src/cli/cmd/tui/component/dialog-image-model.tsx +97 -0
  83. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  84. package/src/cli/cmd/tui/component/dialog-model.tsx +234 -0
  85. package/src/cli/cmd/tui/component/dialog-provider.tsx +260 -0
  86. package/src/cli/cmd/tui/component/dialog-rag-model.tsx +217 -0
  87. package/src/cli/cmd/tui/component/dialog-remote.tsx +489 -0
  88. package/src/cli/cmd/tui/component/dialog-session-list.tsx +170 -0
  89. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  90. package/src/cli/cmd/tui/component/dialog-settings/index.tsx +59 -0
  91. package/src/cli/cmd/tui/component/dialog-settings/prompt.tsx +40 -0
  92. package/src/cli/cmd/tui/component/dialog-settings/sidebar.tsx +39 -0
  93. package/src/cli/cmd/tui/component/dialog-settings/spinner.tsx +62 -0
  94. package/src/cli/cmd/tui/component/dialog-settings/ui.tsx +58 -0
  95. package/src/cli/cmd/tui/component/dialog-skills.tsx +117 -0
  96. package/src/cli/cmd/tui/component/dialog-speak-model.tsx +304 -0
  97. package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
  98. package/src/cli/cmd/tui/component/dialog-status.tsx +165 -0
  99. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  100. package/src/cli/cmd/tui/component/dialog-theme-create.tsx +717 -0
  101. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +52 -0
  102. package/src/cli/cmd/tui/component/dialog-workspace-list.tsx +350 -0
  103. package/src/cli/cmd/tui/component/error-component.tsx +91 -0
  104. package/src/cli/cmd/tui/component/logo.tsx +103 -0
  105. package/src/cli/cmd/tui/component/plugin-route-missing.tsx +14 -0
  106. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +669 -0
  107. package/src/cli/cmd/tui/component/prompt/frecency.tsx +89 -0
  108. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  109. package/src/cli/cmd/tui/component/prompt/index.tsx +2165 -0
  110. package/src/cli/cmd/tui/component/prompt/stash.tsx +63 -0
  111. package/src/cli/cmd/tui/component/spinner.tsx +24 -0
  112. package/src/cli/cmd/tui/component/startup-loading.tsx +63 -0
  113. package/src/cli/cmd/tui/component/table/markdown-table.tsx +267 -0
  114. package/src/cli/cmd/tui/component/table-db/db/connections.ts +75 -0
  115. package/src/cli/cmd/tui/component/table-db/db/db-connection.ts +223 -0
  116. package/src/cli/cmd/tui/component/table-db/db/db-preview.ts +202 -0
  117. package/src/cli/cmd/tui/component/table-db/db/factory.ts +77 -0
  118. package/src/cli/cmd/tui/component/table-db/db/index.ts +9 -0
  119. package/src/cli/cmd/tui/component/table-db/db/mysql-connection.ts +330 -0
  120. package/src/cli/cmd/tui/component/table-db/db/postgres-connection.ts +338 -0
  121. package/src/cli/cmd/tui/component/table-db/db/sqlite-connection.ts +302 -0
  122. package/src/cli/cmd/tui/component/table-db/db/types.ts +108 -0
  123. package/src/cli/cmd/tui/component/table-db/table/dbedit-hooks.ts +74 -0
  124. package/src/cli/cmd/tui/component/table-db/table/index.ts +15 -0
  125. package/src/cli/cmd/tui/component/table-db/table/table-events.ts +54 -0
  126. package/src/cli/cmd/tui/component/table-db/table/table-formatters.ts +191 -0
  127. package/src/cli/cmd/tui/component/table-db/table/table-hooks.ts +105 -0
  128. package/src/cli/cmd/tui/component/table-db/table/table-keyboard-handler.ts +255 -0
  129. package/src/cli/cmd/tui/component/table-db/table/table-layout-engine.ts +208 -0
  130. package/src/cli/cmd/tui/component/table-db/table/table-renderable.ts +486 -0
  131. package/src/cli/cmd/tui/component/table-db/table/table-selection-manager.ts +136 -0
  132. package/src/cli/cmd/tui/component/table-db/table/table-state.ts +198 -0
  133. package/src/cli/cmd/tui/component/table-db/table/types.ts +69 -0
  134. package/src/cli/cmd/tui/component/table-db/ui/db-visualizer.tsx +71 -0
  135. package/src/cli/cmd/tui/component/table-db/ui/index.ts +2 -0
  136. package/src/cli/cmd/tui/component/table-db/ui/table-renderer.ts +607 -0
  137. package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
  138. package/src/cli/cmd/tui/component/tips.tsx +195 -0
  139. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  140. package/src/cli/cmd/tui/context/args.tsx +14 -0
  141. package/src/cli/cmd/tui/context/directory.ts +13 -0
  142. package/src/cli/cmd/tui/context/exit.tsx +24 -0
  143. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  144. package/src/cli/cmd/tui/context/keybind.tsx +102 -0
  145. package/src/cli/cmd/tui/context/kv.tsx +52 -0
  146. package/src/cli/cmd/tui/context/local.tsx +458 -0
  147. package/src/cli/cmd/tui/context/plugin-keybinds.ts +41 -0
  148. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  149. package/src/cli/cmd/tui/context/route.tsx +54 -0
  150. package/src/cli/cmd/tui/context/sdk.tsx +128 -0
  151. package/src/cli/cmd/tui/context/server.tsx +8 -0
  152. package/src/cli/cmd/tui/context/sync.tsx +510 -0
  153. package/src/cli/cmd/tui/context/theme/abyss.json +233 -0
  154. package/src/cli/cmd/tui/context/theme/apple.json +235 -0
  155. package/src/cli/cmd/tui/context/theme/arctic.json +232 -0
  156. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  157. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  158. package/src/cli/cmd/tui/context/theme/ayuai.json +229 -0
  159. package/src/cli/cmd/tui/context/theme/blood.json +229 -0
  160. package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
  161. package/src/cli/cmd/tui/context/theme/catmoe.json +235 -0
  162. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
  163. package/src/cli/cmd/tui/context/theme/catppuccin-latte.json +233 -0
  164. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  165. package/src/cli/cmd/tui/context/theme/catppuccin.json +259 -0
  166. package/src/cli/cmd/tui/context/theme/charcoal.json +230 -0
  167. package/src/cli/cmd/tui/context/theme/chromatic.json +235 -0
  168. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  169. package/src/cli/cmd/tui/context/theme/cosmic.json +234 -0
  170. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  171. package/src/cli/cmd/tui/context/theme/cyber.json +235 -0
  172. package/src/cli/cmd/tui/context/theme/dawnfox.json +229 -0
  173. package/src/cli/cmd/tui/context/theme/dimension.json +235 -0
  174. package/src/cli/cmd/tui/context/theme/dracula-official.json +222 -0
  175. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  176. package/src/cli/cmd/tui/context/theme/dream.json +235 -0
  177. package/src/cli/cmd/tui/context/theme/duo.json +235 -0
  178. package/src/cli/cmd/tui/context/theme/dusk.json +235 -0
  179. package/src/cli/cmd/tui/context/theme/ebony.json +232 -0
  180. package/src/cli/cmd/tui/context/theme/equilibrium.json +232 -0
  181. package/src/cli/cmd/tui/context/theme/ethereal.json +235 -0
  182. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  183. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  184. package/src/cli/cmd/tui/context/theme/fusion.json +235 -0
  185. package/src/cli/cmd/tui/context/theme/ghost.json +235 -0
  186. package/src/cli/cmd/tui/context/theme/github-dark.json +229 -0
  187. package/src/cli/cmd/tui/context/theme/github-dimmed.json +231 -0
  188. package/src/cli/cmd/tui/context/theme/github-light.json +229 -0
  189. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  190. package/src/cli/cmd/tui/context/theme/glass.json +235 -0
  191. package/src/cli/cmd/tui/context/theme/gold.json +235 -0
  192. package/src/cli/cmd/tui/context/theme/gone.json +234 -0
  193. package/src/cli/cmd/tui/context/theme/greyscale.json +229 -0
  194. package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
  195. package/src/cli/cmd/tui/context/theme/hacker.json +229 -0
  196. package/src/cli/cmd/tui/context/theme/holo.json +235 -0
  197. package/src/cli/cmd/tui/context/theme/ink.json +235 -0
  198. package/src/cli/cmd/tui/context/theme/jet.json +233 -0
  199. package/src/cli/cmd/tui/context/theme/kanagawa.json +227 -0
  200. package/src/cli/cmd/tui/context/theme/lavender.json +236 -0
  201. package/src/cli/cmd/tui/context/theme/lightph.json +235 -0
  202. package/src/cli/cmd/tui/context/theme/lucent-orng.json +237 -0
  203. package/src/cli/cmd/tui/context/theme/material-ocean.json +230 -0
  204. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  205. package/src/cli/cmd/tui/context/theme/matrix.json +227 -0
  206. package/src/cli/cmd/tui/context/theme/mercury.json +245 -0
  207. package/src/cli/cmd/tui/context/theme/midnight.json +235 -0
  208. package/src/cli/cmd/tui/context/theme/modern.json +235 -0
  209. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  210. package/src/cli/cmd/tui/context/theme/muted.json +229 -0
  211. package/src/cli/cmd/tui/context/theme/neon.json +229 -0
  212. package/src/cli/cmd/tui/context/theme/neonfusion.json +235 -0
  213. package/src/cli/cmd/tui/context/theme/neutral.json +235 -0
  214. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  215. package/src/cli/cmd/tui/context/theme/nikcli.json +245 -0
  216. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  217. package/src/cli/cmd/tui/context/theme/nordic.json +235 -0
  218. package/src/cli/cmd/tui/context/theme/nova.json +235 -0
  219. package/src/cli/cmd/tui/context/theme/obsidian.json +234 -0
  220. package/src/cli/cmd/tui/context/theme/one-dark.json +231 -0
  221. package/src/cli/cmd/tui/context/theme/one-pro.json +229 -0
  222. package/src/cli/cmd/tui/context/theme/onyx.json +233 -0
  223. package/src/cli/cmd/tui/context/theme/orng.json +249 -0
  224. package/src/cli/cmd/tui/context/theme/osaka-jade.json +240 -0
  225. package/src/cli/cmd/tui/context/theme/oxocarbon.json +229 -0
  226. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  227. package/src/cli/cmd/tui/context/theme/poimandres.json +230 -0
  228. package/src/cli/cmd/tui/context/theme/prism.json +235 -0
  229. package/src/cli/cmd/tui/context/theme/radiant.json +235 -0
  230. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  231. package/src/cli/cmd/tui/context/theme/shadow.json +235 -0
  232. package/src/cli/cmd/tui/context/theme/silicon.json +235 -0
  233. package/src/cli/cmd/tui/context/theme/slate.json +233 -0
  234. package/src/cli/cmd/tui/context/theme/soft.json +235 -0
  235. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  236. package/src/cli/cmd/tui/context/theme/spectrum.json +235 -0
  237. package/src/cli/cmd/tui/context/theme/starlight.json +233 -0
  238. package/src/cli/cmd/tui/context/theme/sunrise.json +235 -0
  239. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  240. package/src/cli/cmd/tui/context/theme/tech.json +235 -0
  241. package/src/cli/cmd/tui/context/theme/tokyonight-storm.json +245 -0
  242. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  243. package/src/cli/cmd/tui/context/theme/vapor.json +235 -0
  244. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  245. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  246. package/src/cli/cmd/tui/context/theme/vivid.json +232 -0
  247. package/src/cli/cmd/tui/context/theme/void.json +235 -0
  248. package/src/cli/cmd/tui/context/theme/vscode.json +235 -0
  249. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  250. package/src/cli/cmd/tui/context/theme/zinc.json +236 -0
  251. package/src/cli/cmd/tui/context/theme.tsx +1303 -0
  252. package/src/cli/cmd/tui/event.ts +48 -0
  253. package/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx +152 -0
  254. package/src/cli/cmd/tui/feature-plugins/home/tips.tsx +50 -0
  255. package/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +63 -0
  256. package/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx +62 -0
  257. package/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx +93 -0
  258. package/src/cli/cmd/tui/feature-plugins/sidebar/lsp.tsx +66 -0
  259. package/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx +96 -0
  260. package/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx +48 -0
  261. package/src/cli/cmd/tui/feature-plugins/system/plugins.tsx +288 -0
  262. package/src/cli/cmd/tui/plugin/api.tsx +407 -0
  263. package/src/cli/cmd/tui/plugin/index.ts +3 -0
  264. package/src/cli/cmd/tui/plugin/internal.ts +25 -0
  265. package/src/cli/cmd/tui/plugin/runtime.ts +1048 -0
  266. package/src/cli/cmd/tui/plugin/slots.tsx +61 -0
  267. package/src/cli/cmd/tui/routes/home.tsx +153 -0
  268. package/src/cli/cmd/tui/routes/session/dbedit.tsx +474 -0
  269. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +65 -0
  270. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +110 -0
  271. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +105 -0
  272. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  273. package/src/cli/cmd/tui/routes/session/footer.tsx +75 -0
  274. package/src/cli/cmd/tui/routes/session/header.tsx +177 -0
  275. package/src/cli/cmd/tui/routes/session/index.tsx +2280 -0
  276. package/src/cli/cmd/tui/routes/session/permission.tsx +540 -0
  277. package/src/cli/cmd/tui/routes/session/question.tsx +435 -0
  278. package/src/cli/cmd/tui/routes/session/sidebar.tsx +313 -0
  279. package/src/cli/cmd/tui/thread.ts +174 -0
  280. package/src/cli/cmd/tui/ui/dialog-alert.tsx +57 -0
  281. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +83 -0
  282. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +204 -0
  283. package/src/cli/cmd/tui/ui/dialog-help.tsx +38 -0
  284. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +102 -0
  285. package/src/cli/cmd/tui/ui/dialog-select.tsx +389 -0
  286. package/src/cli/cmd/tui/ui/dialog.tsx +180 -0
  287. package/src/cli/cmd/tui/ui/link.tsx +34 -0
  288. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  289. package/src/cli/cmd/tui/ui/toast.tsx +138 -0
  290. package/src/cli/cmd/tui/util/clipboard.ts +154 -0
  291. package/src/cli/cmd/tui/util/editor.ts +32 -0
  292. package/src/cli/cmd/tui/util/signal.ts +7 -0
  293. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  294. package/src/cli/cmd/tui/util/transcript.ts +98 -0
  295. package/src/cli/cmd/tui/win32.ts +110 -0
  296. package/src/cli/cmd/tui/worker.ts +156 -0
  297. package/src/cli/cmd/uninstall.ts +357 -0
  298. package/src/cli/cmd/upgrade.ts +72 -0
  299. package/src/cli/cmd/web.ts +87 -0
  300. package/src/cli/cmd/workspace-serve.ts +16 -0
  301. package/src/cli/error.ts +57 -0
  302. package/src/cli/network.ts +55 -0
  303. package/src/cli/remote/index.ts +36 -0
  304. package/src/cli/remote/notifications.ts +104 -0
  305. package/src/cli/remote/qr-renderer.ts +86 -0
  306. package/src/cli/remote/remote-service.ts +757 -0
  307. package/src/cli/remote/session-manager.ts +284 -0
  308. package/src/cli/remote/subagent-hooks.ts +151 -0
  309. package/src/cli/remote/types.ts +121 -0
  310. package/src/cli/ui.ts +96 -0
  311. package/src/cli/upgrade.ts +25 -0
  312. package/src/command/index.ts +174 -0
  313. package/src/command/template/initialize.txt +10 -0
  314. package/src/command/template/review.txt +99 -0
  315. package/src/config/config.ts +1760 -0
  316. package/src/config/markdown.ts +88 -0
  317. package/src/config/migrate-tui-config.ts +155 -0
  318. package/src/config/paths.ts +174 -0
  319. package/src/config/tui-schema.ts +36 -0
  320. package/src/config/tui.ts +209 -0
  321. package/src/connectors/api/base.ts +75 -0
  322. package/src/connectors/api/figma.ts +103 -0
  323. package/src/connectors/api/github.ts +247 -0
  324. package/src/connectors/api/lovable.ts +126 -0
  325. package/src/connectors/api/slack.ts +137 -0
  326. package/src/connectors/auth.ts +68 -0
  327. package/src/connectors/cache.ts +119 -0
  328. package/src/connectors/credentials.ts +81 -0
  329. package/src/connectors/index.ts +202 -0
  330. package/src/connectors/registry.ts +358 -0
  331. package/src/docs/context.ts +120 -0
  332. package/src/docs/library.ts +189 -0
  333. package/src/env/index.ts +26 -0
  334. package/src/file/ignore.ts +83 -0
  335. package/src/file/index.ts +411 -0
  336. package/src/file/ripgrep.ts +402 -0
  337. package/src/file/time.ts +65 -0
  338. package/src/file/watcher.ts +127 -0
  339. package/src/flag/flag.ts +128 -0
  340. package/src/format/formatter.ts +356 -0
  341. package/src/format/index.ts +137 -0
  342. package/src/global/index.ts +57 -0
  343. package/src/id/id.ts +83 -0
  344. package/src/ide/index.ts +76 -0
  345. package/src/index.ts +184 -0
  346. package/src/installation/index.ts +246 -0
  347. package/src/lsp/client.ts +250 -0
  348. package/src/lsp/index.ts +483 -0
  349. package/src/lsp/language.ts +119 -0
  350. package/src/lsp/server.ts +2046 -0
  351. package/src/mcp/auth.ts +121 -0
  352. package/src/mcp/index.ts +860 -0
  353. package/src/mcp/oauth-callback.ts +198 -0
  354. package/src/mcp/oauth-provider.ts +148 -0
  355. package/src/mobile/auth.ts +97 -0
  356. package/src/mobile/github-repo.ts +185 -0
  357. package/src/patch/index.ts +631 -0
  358. package/src/permission/arity.ts +150 -0
  359. package/src/permission/dbedit.ts +236 -0
  360. package/src/permission/index.ts +210 -0
  361. package/src/permission/next.ts +287 -0
  362. package/src/plugin/codex.ts +493 -0
  363. package/src/plugin/copilot.ts +261 -0
  364. package/src/plugin/index.ts +714 -0
  365. package/src/plugin/install.ts +379 -0
  366. package/src/plugin/meta.ts +165 -0
  367. package/src/plugin/shared.ts +188 -0
  368. package/src/project/bootstrap.ts +35 -0
  369. package/src/project/instance.ts +84 -0
  370. package/src/project/project.ts +373 -0
  371. package/src/project/state.ts +66 -0
  372. package/src/project/vcs.ts +76 -0
  373. package/src/prompt/stash-store.ts +93 -0
  374. package/src/provider/auth.ts +147 -0
  375. package/src/provider/models-macro.ts +22 -0
  376. package/src/provider/models.ts +216 -0
  377. package/src/provider/provider.ts +1483 -0
  378. package/src/provider/sdk/openai-compatible/src/README.md +5 -0
  379. package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
  380. package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +100 -0
  381. package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
  382. package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +22 -0
  383. package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
  384. package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
  385. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
  386. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1732 -0
  387. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
  388. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
  389. package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
  390. package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
  391. package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
  392. package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
  393. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
  394. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
  395. package/src/provider/transform.ts +828 -0
  396. package/src/pty/index.ts +241 -0
  397. package/src/question/index.ts +171 -0
  398. package/src/rag/chunk.ts +43 -0
  399. package/src/rag/embed.ts +179 -0
  400. package/src/rag/index.ts +376 -0
  401. package/src/rag/storage.ts +76 -0
  402. package/src/scheduler/index.ts +61 -0
  403. package/src/server/error.ts +36 -0
  404. package/src/server/event.ts +7 -0
  405. package/src/server/mdns.ts +59 -0
  406. package/src/server/routes/chatbot.ts +205 -0
  407. package/src/server/routes/companion.ts +729 -0
  408. package/src/server/routes/config.ts +92 -0
  409. package/src/server/routes/connectors.ts +121 -0
  410. package/src/server/routes/dbedit.ts +76 -0
  411. package/src/server/routes/experimental.ts +210 -0
  412. package/src/server/routes/file.ts +197 -0
  413. package/src/server/routes/global.ts +135 -0
  414. package/src/server/routes/mcp.ts +225 -0
  415. package/src/server/routes/mobile.ts +2044 -0
  416. package/src/server/routes/permission.ts +68 -0
  417. package/src/server/routes/project.ts +82 -0
  418. package/src/server/routes/provider.ts +235 -0
  419. package/src/server/routes/pty.ts +169 -0
  420. package/src/server/routes/question.ts +98 -0
  421. package/src/server/routes/session.ts +968 -0
  422. package/src/server/routes/tui.ts +379 -0
  423. package/src/server/routes/workspace.ts +104 -0
  424. package/src/server/server.ts +761 -0
  425. package/src/server/ssh.ts +207 -0
  426. package/src/session/auth.ts +402 -0
  427. package/src/session/compaction.ts +253 -0
  428. package/src/session/generate.ts +38 -0
  429. package/src/session/index.ts +598 -0
  430. package/src/session/llm.ts +273 -0
  431. package/src/session/message-v2.ts +836 -0
  432. package/src/session/message.ts +189 -0
  433. package/src/session/processor.ts +408 -0
  434. package/src/session/prompt/anthropic-20250930.txt +165 -0
  435. package/src/session/prompt/anthropic.txt +105 -0
  436. package/src/session/prompt/anthropic_spoof.txt +1 -0
  437. package/src/session/prompt/beast.txt +147 -0
  438. package/src/session/prompt/build-switch.txt +5 -0
  439. package/src/session/prompt/codex_header.txt +79 -0
  440. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  441. package/src/session/prompt/gemini.txt +155 -0
  442. package/src/session/prompt/max-steps.txt +16 -0
  443. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  444. package/src/session/prompt/plan.txt +25 -0
  445. package/src/session/prompt/qwen.txt +108 -0
  446. package/src/session/prompt.ts +1942 -0
  447. package/src/session/retry.ts +90 -0
  448. package/src/session/revert.ts +120 -0
  449. package/src/session/stats.ts +404 -0
  450. package/src/session/status.ts +84 -0
  451. package/src/session/summary.ts +184 -0
  452. package/src/session/system.ts +195 -0
  453. package/src/session/toast.tsx +105 -0
  454. package/src/session/todo.ts +258 -0
  455. package/src/session/uninstall.ts +357 -0
  456. package/src/share/share-next.ts +421 -0
  457. package/src/share/share.ts +92 -0
  458. package/src/shell/shell.ts +65 -0
  459. package/src/skill/index.ts +1 -0
  460. package/src/skill/skill.ts +232 -0
  461. package/src/snapshot/index.ts +297 -0
  462. package/src/storage/storage.ts +227 -0
  463. package/src/tool/apply_patch.ts +288 -0
  464. package/src/tool/apply_patch.txt +33 -0
  465. package/src/tool/bash.ts +252 -0
  466. package/src/tool/bash.txt +115 -0
  467. package/src/tool/batch.ts +175 -0
  468. package/src/tool/batch.txt +24 -0
  469. package/src/tool/codesearch.ts +132 -0
  470. package/src/tool/codesearch.txt +12 -0
  471. package/src/tool/context_collect.ts +152 -0
  472. package/src/tool/context_collect.txt +9 -0
  473. package/src/tool/context_diagnostics.ts +81 -0
  474. package/src/tool/context_diagnostics.txt +5 -0
  475. package/src/tool/context_related.ts +117 -0
  476. package/src/tool/context_related.txt +5 -0
  477. package/src/tool/context_search.ts +108 -0
  478. package/src/tool/context_search.txt +8 -0
  479. package/src/tool/db-diff.ts +434 -0
  480. package/src/tool/db-table.txt +15 -0
  481. package/src/tool/docs_add.ts +50 -0
  482. package/src/tool/docs_add.txt +5 -0
  483. package/src/tool/docs_context.ts +56 -0
  484. package/src/tool/docs_context.txt +4 -0
  485. package/src/tool/docs_gap_report.ts +79 -0
  486. package/src/tool/docs_gap_report.txt +7 -0
  487. package/src/tool/docs_load.ts +41 -0
  488. package/src/tool/docs_load.txt +4 -0
  489. package/src/tool/docs_request.ts +129 -0
  490. package/src/tool/docs_request.txt +7 -0
  491. package/src/tool/docs_search.ts +51 -0
  492. package/src/tool/docs_search.txt +6 -0
  493. package/src/tool/docs_unload.ts +38 -0
  494. package/src/tool/docs_unload.txt +5 -0
  495. package/src/tool/edit.ts +614 -0
  496. package/src/tool/edit.txt +10 -0
  497. package/src/tool/external-directory.ts +32 -0
  498. package/src/tool/generate_image.ts +174 -0
  499. package/src/tool/generate_image.txt +12 -0
  500. package/src/tool/glob.ts +79 -0
  501. package/src/tool/glob.txt +6 -0
  502. package/src/tool/grep.ts +153 -0
  503. package/src/tool/grep.txt +8 -0
  504. package/src/tool/invalid.ts +17 -0
  505. package/src/tool/ls.ts +116 -0
  506. package/src/tool/ls.txt +1 -0
  507. package/src/tool/lsp.ts +96 -0
  508. package/src/tool/lsp.txt +19 -0
  509. package/src/tool/memory_search.ts +141 -0
  510. package/src/tool/memory_search.txt +8 -0
  511. package/src/tool/multiedit.ts +46 -0
  512. package/src/tool/multiedit.txt +41 -0
  513. package/src/tool/plan-enter.txt +14 -0
  514. package/src/tool/plan-exit.txt +13 -0
  515. package/src/tool/plan.ts +130 -0
  516. package/src/tool/question.ts +33 -0
  517. package/src/tool/question.txt +10 -0
  518. package/src/tool/rag_index.ts +77 -0
  519. package/src/tool/rag_index.txt +10 -0
  520. package/src/tool/rag_reset.ts +26 -0
  521. package/src/tool/rag_reset.txt +4 -0
  522. package/src/tool/rag_search.ts +62 -0
  523. package/src/tool/rag_search.txt +6 -0
  524. package/src/tool/rag_status.ts +45 -0
  525. package/src/tool/rag_status.txt +4 -0
  526. package/src/tool/read.ts +203 -0
  527. package/src/tool/read.txt +12 -0
  528. package/src/tool/registry.ts +214 -0
  529. package/src/tool/skill.ts +169 -0
  530. package/src/tool/skill.txt +3 -0
  531. package/src/tool/smart_docs.ts +74 -0
  532. package/src/tool/smart_docs.txt +7 -0
  533. package/src/tool/speak/elevenlabs.ts +201 -0
  534. package/src/tool/speak/openrouter.ts +240 -0
  535. package/src/tool/speak/provider.ts +83 -0
  536. package/src/tool/speak.ts +440 -0
  537. package/src/tool/task.ts +194 -0
  538. package/src/tool/task.txt +60 -0
  539. package/src/tool/todo.ts +53 -0
  540. package/src/tool/todoread.txt +14 -0
  541. package/src/tool/todowrite.txt +167 -0
  542. package/src/tool/tool.ts +87 -0
  543. package/src/tool/tree.ts +218 -0
  544. package/src/tool/tree.txt +8 -0
  545. package/src/tool/truncation.ts +106 -0
  546. package/src/tool/use-connector.ts +47 -0
  547. package/src/tool/voice.ts +188 -0
  548. package/src/tool/webfetch.ts +205 -0
  549. package/src/tool/webfetch.txt +13 -0
  550. package/src/tool/websearch.ts +150 -0
  551. package/src/tool/websearch.txt +14 -0
  552. package/src/tool/write.ts +80 -0
  553. package/src/tool/write.txt +8 -0
  554. package/src/util/archive.ts +16 -0
  555. package/src/util/color.ts +19 -0
  556. package/src/util/context.ts +25 -0
  557. package/src/util/defer.ts +12 -0
  558. package/src/util/error.ts +77 -0
  559. package/src/util/eventloop.ts +20 -0
  560. package/src/util/filesystem.ts +125 -0
  561. package/src/util/flock.ts +329 -0
  562. package/src/util/fn.ts +11 -0
  563. package/src/util/format.ts +20 -0
  564. package/src/util/hash.ts +7 -0
  565. package/src/util/iife.ts +3 -0
  566. package/src/util/keybind.ts +103 -0
  567. package/src/util/lazy.ts +18 -0
  568. package/src/util/locale.ts +81 -0
  569. package/src/util/lock.ts +98 -0
  570. package/src/util/log.ts +180 -0
  571. package/src/util/network.ts +9 -0
  572. package/src/util/process.ts +15 -0
  573. package/src/util/queue.ts +32 -0
  574. package/src/util/record.ts +3 -0
  575. package/src/util/rpc.ts +66 -0
  576. package/src/util/scrap.ts +10 -0
  577. package/src/util/signal.ts +12 -0
  578. package/src/util/timeout.ts +14 -0
  579. package/src/util/token.ts +7 -0
  580. package/src/util/wildcard.ts +56 -0
  581. package/src/workspace/adaptors/index.ts +271 -0
  582. package/src/workspace/adaptors/types.ts +14 -0
  583. package/src/workspace/adaptors/worktree.ts +31 -0
  584. package/src/workspace/config.ts +19 -0
  585. package/src/workspace/index.ts +223 -0
  586. package/src/workspace/session-proxy-middleware.ts +97 -0
  587. package/src/workspace/sse.ts +66 -0
  588. package/src/workspace/workspace-context.ts +23 -0
  589. package/src/workspace/workspace-server/routes.ts +33 -0
  590. package/src/workspace/workspace-server/server.ts +47 -0
  591. package/src/worktree/index.ts +487 -0
  592. package/sst-env.d.ts +10 -0
  593. package/test/benchmark.test.ts +121 -0
  594. package/test/build-optimizations.test.ts +124 -0
  595. package/test/id-benchmark.test.ts +132 -0
  596. package/test/optimizations.test.ts +302 -0
  597. package/test/preload.ts +1 -0
  598. package/test/solidjs-benchmark.test.ts +262 -0
  599. package/test/solidjs-optimizations.test.ts +259 -0
  600. package/test/tui-benchmark.test.ts +230 -0
  601. package/test/wildcard-benchmark.test.ts +180 -0
  602. package/tsconfig.json +26 -0
@@ -0,0 +1,2044 @@
1
+ import path from "path"
2
+ import { Hono } from "hono"
3
+ import { describeRoute, resolver, validator } from "hono-openapi"
4
+ import { streamSSE } from "hono/streaming"
5
+ import { NamedError } from "@nikcli-ai/util/error"
6
+ import z from "zod"
7
+ import { Bus } from "@/bus"
8
+ import { Instance } from "@/project/instance"
9
+ import { Project } from "@/project/project"
10
+ import { Session } from "@/session"
11
+ import { SessionPrompt } from "@/session/prompt"
12
+ import { SessionStatus } from "@/session/status"
13
+ import { SessionSummary } from "@/session/summary"
14
+ import { MessageV2 } from "@/session/message-v2"
15
+ import { Agent } from "@/agent/agent"
16
+ import { PermissionNext } from "@/permission/next"
17
+ import { Provider } from "@/provider/provider"
18
+ import { GlobalBus } from "@/bus/global"
19
+ import { Snapshot } from "@/snapshot"
20
+ import { Worktree } from "@/worktree"
21
+ import { GithubApi } from "@/connectors/api/github"
22
+ import { ConnectorAuth } from "@/connectors/auth"
23
+ import { Connectors } from "@/connectors"
24
+ import { Installation } from "@/installation"
25
+ import { Global } from "@/global"
26
+ import { MobileAuth } from "@/mobile/auth"
27
+ import { MobileGithubRepo } from "@/mobile/github-repo"
28
+ import { Storage } from "@/storage/storage"
29
+ import { Flag } from "@/flag/flag"
30
+ import { Config } from "@/config/config"
31
+ import { Command } from "@/command"
32
+ import { Workspace } from "@/workspace"
33
+ import { WorkspaceContext } from "@/workspace/workspace-context"
34
+ import { getContainerRuntimeInfo } from "@/workspace/adaptors"
35
+ import { proxyWorkspaceRequest } from "@/workspace/session-proxy-middleware"
36
+ import { PromptStashStore } from "@/prompt/stash-store"
37
+ import { errors } from "../error"
38
+ import { lazy } from "@/util/lazy"
39
+ import { Log } from "@/util/log"
40
+
41
+ const log = Log.create({ service: "mobile-routes" })
42
+
43
+ const MobileProject = Project.Info.extend({ current: z.boolean() }).meta({ ref: "MobileProject" })
44
+ const MobileExecutionTarget = z.enum(["local", "container"]).meta({ ref: "MobileExecutionTarget" })
45
+
46
+ const MobileBootstrap = z
47
+ .object({
48
+ version: z.string(),
49
+ auth: z.object({
50
+ bearerEnabled: z.boolean(),
51
+ currentToken: MobileAuth.PublicToken.optional(),
52
+ }),
53
+ currentProject: MobileProject,
54
+ projects: MobileProject.array(),
55
+ execution: z.object({
56
+ container: z.object({
57
+ available: z.boolean(),
58
+ runtime: z.enum(["docker", "podman"]).optional(),
59
+ image: z.string(),
60
+ }),
61
+ }),
62
+ github: z.object({
63
+ connected: z.boolean(),
64
+ oauthDeviceEnabled: z.boolean(),
65
+ oauthDeviceConfigured: z.boolean().optional(),
66
+ oauthClientSource: z.enum(["flag", "config", "env"]).optional(),
67
+ user: z
68
+ .object({
69
+ login: z.string(),
70
+ name: z.string().nullable().optional(),
71
+ avatar_url: z.string().optional(),
72
+ })
73
+ .optional(),
74
+ }),
75
+ })
76
+ .meta({ ref: "MobileBootstrap" })
77
+
78
+ const MobileSessionSummary = z
79
+ .object({
80
+ info: Session.Info,
81
+ status: SessionStatus.Info.optional(),
82
+ })
83
+ .meta({ ref: "MobileSessionSummary" })
84
+
85
+ const MobileSessionDetail = z
86
+ .object({
87
+ info: Session.Info,
88
+ status: SessionStatus.Info.optional(),
89
+ messages: MessageV2.WithParts.array(),
90
+ permissions: PermissionNext.Request.array(),
91
+ })
92
+ .meta({ ref: "MobileSessionDetail" })
93
+
94
+ const GithubAuthInput = z.object({ token: z.string().min(1) })
95
+
96
+ const MobileGithubBranch = z
97
+ .object({
98
+ name: z.string(),
99
+ protected: z.boolean().optional(),
100
+ commit: z.object({
101
+ sha: z.string(),
102
+ }),
103
+ })
104
+ .meta({ ref: "MobileGithubBranch" })
105
+
106
+ const MobileGithubSessionCreateInput = z
107
+ .object({
108
+ owner: z.string().min(1),
109
+ repo: z.string().min(1),
110
+ cloneUrl: z.url(),
111
+ htmlUrl: z.url().optional(),
112
+ defaultBranch: z.string().min(1),
113
+ baseBranch: z.string().min(1),
114
+ private: z.boolean().default(false),
115
+ title: z.string().optional(),
116
+ executionTarget: MobileExecutionTarget.default("local"),
117
+ })
118
+ .meta({ ref: "MobileGithubSessionCreateInput" })
119
+
120
+ const MobileSessionCreateInput = z
121
+ .object({
122
+ parentID: Session.Info.shape.parentID,
123
+ title: Session.Info.shape.title.optional(),
124
+ permission: Session.Info.shape.permission,
125
+ github: Session.Info.shape.github.optional(),
126
+ executionTarget: MobileExecutionTarget.default("local"),
127
+ })
128
+ .optional()
129
+ .meta({ ref: "MobileSessionCreateInput" })
130
+
131
+ const MobileGithubSessionCreateResult = z
132
+ .object({
133
+ session: Session.Info,
134
+ worktree: Worktree.Info,
135
+ project: Project.Info,
136
+ workspace: Workspace.Info.optional(),
137
+ })
138
+ .meta({ ref: "MobileGithubSessionCreateResult" })
139
+
140
+ const MobileCommand = z
141
+ .object({
142
+ name: z.string(),
143
+ description: z.string().optional(),
144
+ agent: z.string().optional(),
145
+ model: z.string().optional(),
146
+ mcp: z.boolean().optional(),
147
+ skill: z.boolean().optional(),
148
+ subtask: z.boolean().optional(),
149
+ hints: z.array(z.string()),
150
+ })
151
+ .meta({ ref: "MobileCommand" })
152
+
153
+ const MobileSessionCommandInput = z
154
+ .object({
155
+ command: z.string().min(1),
156
+ arguments: z.string().default(""),
157
+ agent: z.string().optional(),
158
+ model: z
159
+ .object({
160
+ providerID: z.string(),
161
+ modelID: z.string(),
162
+ })
163
+ .optional(),
164
+ })
165
+ .meta({ ref: "MobileSessionCommandInput" })
166
+
167
+ const MobileGithubPublishInput = z
168
+ .object({
169
+ title: z.string().optional(),
170
+ body: z.string().optional(),
171
+ commitMessage: z.string().optional(),
172
+ })
173
+ .optional()
174
+ .meta({ ref: "MobileGithubPublishInput" })
175
+
176
+ const MobileGithubPublishResult = z
177
+ .object({
178
+ commitSha: z.string(),
179
+ branch: z.string(),
180
+ pullRequest: z.object({
181
+ number: z.number(),
182
+ url: z.string(),
183
+ title: z.string(),
184
+ }),
185
+ })
186
+ .meta({ ref: "MobileGithubPublishResult" })
187
+
188
+ const MobileGithubDeviceAuthStart = z
189
+ .object({
190
+ deviceCode: z.string(),
191
+ userCode: z.string(),
192
+ verificationUri: z.string(),
193
+ verificationUriComplete: z.string().optional(),
194
+ expiresAt: z.number(),
195
+ interval: z.number(),
196
+ })
197
+ .meta({ ref: "MobileGithubDeviceAuthStart" })
198
+
199
+ const MobileGithubDeviceAuthPollInput = z
200
+ .object({
201
+ deviceCode: z.string().min(1),
202
+ })
203
+ .meta({ ref: "MobileGithubDeviceAuthPollInput" })
204
+
205
+ const MobileGithubDeviceAuthPollResult = z
206
+ .object({
207
+ status: z.enum(["pending", "approved", "denied", "expired"]),
208
+ interval: z.number().optional(),
209
+ user: z
210
+ .object({
211
+ login: z.string(),
212
+ name: z.string().nullable().optional(),
213
+ avatar_url: z.string().optional(),
214
+ })
215
+ .optional(),
216
+ })
217
+ .meta({ ref: "MobileGithubDeviceAuthPollResult" })
218
+
219
+ const MobilePromptHistoryEntry = z
220
+ .object({
221
+ id: z.string(),
222
+ input: z.string(),
223
+ mode: z.enum(["normal", "shell"]).optional(),
224
+ partsCount: z.number(),
225
+ })
226
+ .meta({ ref: "MobilePromptHistoryEntry" })
227
+
228
+ const MobilePromptStashEntry = z
229
+ .object({
230
+ id: z.string(),
231
+ input: z.string(),
232
+ timestamp: z.number(),
233
+ partsCount: z.number(),
234
+ })
235
+ .meta({ ref: "MobilePromptStashEntry" })
236
+
237
+ const MobilePromptStashCreateInput = z
238
+ .object({
239
+ input: z.string().trim().min(1),
240
+ })
241
+ .meta({ ref: "MobilePromptStashCreateInput" })
242
+
243
+ const MobileMemorySearchHit = z
244
+ .object({
245
+ id: z.string(),
246
+ sessionID: z.string(),
247
+ sessionTitle: z.string(),
248
+ messageID: z.string(),
249
+ role: z.enum(["user", "assistant"]),
250
+ createdAt: z.number(),
251
+ preview: z.string(),
252
+ })
253
+ .meta({ ref: "MobileMemorySearchHit" })
254
+
255
+ function currentToken(c: any) {
256
+ return (c.get("mobileAuth") as MobileAuth.PublicToken | undefined) ?? undefined
257
+ }
258
+
259
+ type PromptHistoryRecord = {
260
+ input: string
261
+ mode?: "normal" | "shell"
262
+ parts?: unknown[]
263
+ }
264
+
265
+ type PromptStashRecord = {
266
+ input: string
267
+ timestamp: number
268
+ parts?: unknown[]
269
+ }
270
+
271
+ async function readJsonLines<T>(filePath: string) {
272
+ const text = await Bun.file(filePath)
273
+ .text()
274
+ .catch(() => "")
275
+ return text
276
+ .split("\n")
277
+ .filter(Boolean)
278
+ .map((line) => {
279
+ try {
280
+ return JSON.parse(line) as T
281
+ } catch {
282
+ return null
283
+ }
284
+ })
285
+ .filter((item): item is T => item !== null)
286
+ }
287
+
288
+ function historyFilePath() {
289
+ return path.join(Global.Path.state, "prompt-history.jsonl")
290
+ }
291
+
292
+ function stashFilePath() {
293
+ return path.join(Global.Path.state, "prompt-stash.jsonl")
294
+ }
295
+
296
+ async function listPromptHistory() {
297
+ const entries = await readJsonLines<PromptHistoryRecord>(historyFilePath())
298
+ return entries
299
+ .filter((entry): entry is PromptHistoryRecord => typeof entry.input === "string")
300
+ .slice(-50)
301
+ .reverse()
302
+ .map((entry, index) => ({
303
+ id: `${index}`,
304
+ input: entry.input,
305
+ mode: entry.mode === "shell" ? "shell" : entry.mode === "normal" ? "normal" : undefined,
306
+ partsCount: Array.isArray(entry.parts) ? entry.parts.length : 0,
307
+ }))
308
+ }
309
+
310
+ async function listPromptStash() {
311
+ const entries = await PromptStashStore.list()
312
+ return entries
313
+ .sort((a, b) => b.timestamp - a.timestamp)
314
+ .map((entry) => ({
315
+ id: entry.id,
316
+ input: entry.input,
317
+ timestamp: entry.timestamp,
318
+ partsCount: Array.isArray(entry.parts) ? entry.parts.length : 0,
319
+ }))
320
+ }
321
+
322
+ function messageSearchText(message: MessageV2.WithParts) {
323
+ const text = message.parts
324
+ .filter((part): part is Extract<MessageV2.WithParts["parts"][number], { type: "text" }> => part.type === "text")
325
+ .map((part) => part.text)
326
+ .join("\n\n")
327
+ .trim()
328
+ if (text) return text
329
+ if (message.info.role === "assistant") {
330
+ return message.info.error?.data?.message?.trim() ?? ""
331
+ }
332
+ return ""
333
+ }
334
+
335
+ function snippetForQuery(text: string, query: string) {
336
+ const lower = text.toLowerCase()
337
+ const index = lower.indexOf(query)
338
+ if (index === -1) return text.slice(0, 180)
339
+ const start = Math.max(0, index - 48)
340
+ const end = Math.min(text.length, index + query.length + 108)
341
+ const prefix = start > 0 ? "..." : ""
342
+ const suffix = end < text.length ? "..." : ""
343
+ return `${prefix}${text.slice(start, end).trim()}${suffix}`
344
+ }
345
+
346
+ async function searchPromptMemories(query: string) {
347
+ const normalized = query.trim().toLowerCase()
348
+ if (!normalized) return []
349
+ const hits: Array<{
350
+ id: string
351
+ sessionID: string
352
+ sessionTitle: string
353
+ messageID: string
354
+ role: "user" | "assistant"
355
+ createdAt: number
356
+ preview: string
357
+ }> = []
358
+
359
+ const sessionKeys = await Storage.list(["session"])
360
+ for (const key of sessionKeys) {
361
+ if (key.length !== 3) continue
362
+ const session = await Storage.read<Session.Info>(key).catch(() => undefined)
363
+ if (!session) continue
364
+ const messages = await Session.messages({ sessionID: session.id }).catch(() => [])
365
+ for (const message of messages) {
366
+ const text = messageSearchText(message)
367
+ if (!text || !text.toLowerCase().includes(normalized)) continue
368
+ hits.push({
369
+ id: `${session.id}:${message.info.id}`,
370
+ sessionID: session.id,
371
+ sessionTitle: session.title,
372
+ messageID: message.info.id,
373
+ role: message.info.role,
374
+ createdAt: message.info.time.created,
375
+ preview: snippetForQuery(text, normalized),
376
+ })
377
+ if (hits.length >= 40) break
378
+ }
379
+ if (hits.length >= 40) break
380
+ }
381
+
382
+ return hits.sort((a, b) => b.createdAt - a.createdAt).slice(0, 24)
383
+ }
384
+
385
+ async function latestPromptDefaults(sessionID: string) {
386
+ const messages = await Session.messages({ sessionID, limit: 24 }).catch(() => [])
387
+ for (let index = messages.length - 1; index >= 0; index--) {
388
+ const info = messages[index]?.info
389
+ if (!info || info.role !== "user") continue
390
+ return {
391
+ agent: info.agent,
392
+ model: info.model,
393
+ }
394
+ }
395
+ return {}
396
+ }
397
+
398
+ async function resolveMobilePromptDefaults(session: Session.Info) {
399
+ return Instance.provide({
400
+ directory: session.directory,
401
+ async fn() {
402
+ const current = await latestPromptDefaults(session.id)
403
+ if (current.agent && current.model) return current
404
+
405
+ const allKeys = await Storage.list(["session"])
406
+ const sessions: Session.Info[] = []
407
+ for (const key of allKeys) {
408
+ if (key.length !== 3 || key[2] === session.id) continue
409
+ const candidate = await Storage.read<Session.Info>(key).catch(() => undefined)
410
+ if (!candidate || candidate.projectID !== session.projectID) continue
411
+ sessions.push(candidate)
412
+ }
413
+
414
+ sessions.sort((a, b) => b.time.updated - a.time.updated)
415
+
416
+ for (const candidate of sessions) {
417
+ const fallback = await latestPromptDefaults(candidate.id)
418
+ if (!fallback.agent || !fallback.model) continue
419
+ return {
420
+ agent: current.agent ?? fallback.agent,
421
+ model: current.model ?? fallback.model,
422
+ }
423
+ }
424
+
425
+ return {
426
+ agent: current.agent ?? (await Agent.defaultAgent()),
427
+ model: current.model ?? (await Provider.defaultModel()),
428
+ }
429
+ },
430
+ })
431
+ }
432
+
433
+ function extractSessionIDs(value: unknown): string[] {
434
+ if (!value || typeof value !== "object") return []
435
+ const result = new Set<string>()
436
+ const visit = (input: unknown) => {
437
+ if (!input || typeof input !== "object") return
438
+ if (Array.isArray(input)) {
439
+ for (const item of input) visit(item)
440
+ return
441
+ }
442
+ for (const [key, current] of Object.entries(input)) {
443
+ if ((key === "sessionID" || key === "id") && typeof current === "string" && current.startsWith("ses_")) {
444
+ result.add(current)
445
+ }
446
+ if (current && typeof current === "object") visit(current)
447
+ }
448
+ }
449
+ visit(value)
450
+ return [...result]
451
+ }
452
+
453
+ async function githubToken() {
454
+ const auth = await ConnectorAuth.get("github")
455
+ return auth?.token
456
+ }
457
+
458
+ async function githubOAuthClientID() {
459
+ const config = await Config.get().catch(() => undefined)
460
+ const githubConnector = Object.values(config?.connectors ?? {}).find(
461
+ (connector): connector is Config.ConnectorGithub =>
462
+ typeof connector === "object" && connector !== null && "type" in connector && connector.type === "github",
463
+ )
464
+
465
+ const flagValue = Flag.NIKCLI_GITHUB_OAUTH_CLIENT_ID
466
+ if (flagValue) {
467
+ return {
468
+ clientID: flagValue,
469
+ source: "flag" as const,
470
+ }
471
+ }
472
+
473
+ const configValue = githubConnector?.oauthClientId || githubConnector?.clientId
474
+ if (configValue) {
475
+ return {
476
+ clientID: configValue,
477
+ source: "config" as const,
478
+ }
479
+ }
480
+
481
+ const envValue =
482
+ process.env.NIKCLI_GITHUB_OAUTH_CLIENT_ID || process.env.GITHUB_CLIENT_ID_CONSOLE || process.env.GITHUB_CLIENT_ID
483
+
484
+ if (envValue) {
485
+ return {
486
+ clientID: envValue,
487
+ source: "env" as const,
488
+ }
489
+ }
490
+
491
+ return {
492
+ clientID: undefined,
493
+ source: undefined,
494
+ }
495
+ }
496
+
497
+ async function startGithubDeviceAuth() {
498
+ const { clientID } = await githubOAuthClientID()
499
+ if (!clientID) throw new Error("GitHub OAuth client ID is not configured on the host")
500
+ const response = await fetch("https://github.com/login/device/code", {
501
+ method: "POST",
502
+ headers: {
503
+ Accept: "application/json",
504
+ "Content-Type": "application/json",
505
+ "User-Agent": "nikcli-mobile",
506
+ },
507
+ body: JSON.stringify({
508
+ client_id: clientID,
509
+ scope: "repo read:user user:email",
510
+ }),
511
+ })
512
+ if (!response.ok) {
513
+ throw new Error(`GitHub device auth failed: ${response.status} ${response.statusText}`)
514
+ }
515
+ const payload = (await response.json()) as {
516
+ device_code: string
517
+ user_code: string
518
+ verification_uri: string
519
+ verification_uri_complete?: string
520
+ expires_in: number
521
+ interval?: number
522
+ }
523
+ return {
524
+ deviceCode: payload.device_code,
525
+ userCode: payload.user_code,
526
+ verificationUri: payload.verification_uri,
527
+ verificationUriComplete: payload.verification_uri_complete,
528
+ expiresAt: Date.now() + payload.expires_in * 1000,
529
+ interval: payload.interval ?? 5,
530
+ }
531
+ }
532
+
533
+ async function pollGithubDeviceAuth(deviceCode: string) {
534
+ const { clientID } = await githubOAuthClientID()
535
+ if (!clientID) throw new Error("GitHub OAuth client ID is not configured on the host")
536
+ const response = await fetch("https://github.com/login/oauth/access_token", {
537
+ method: "POST",
538
+ headers: {
539
+ Accept: "application/json",
540
+ "Content-Type": "application/json",
541
+ "User-Agent": "nikcli-mobile",
542
+ },
543
+ body: JSON.stringify({
544
+ client_id: clientID,
545
+ device_code: deviceCode,
546
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
547
+ }),
548
+ })
549
+ if (!response.ok) {
550
+ throw new Error(`GitHub auth polling failed: ${response.status} ${response.statusText}`)
551
+ }
552
+ const payload = (await response.json()) as {
553
+ access_token?: string
554
+ error?: string
555
+ interval?: number
556
+ }
557
+ if (payload.access_token) {
558
+ const user = await GithubApi.getUser(payload.access_token)
559
+ await ConnectorAuth.set("github", { token: payload.access_token })
560
+ Connectors.invalidateConnector("github")
561
+ return {
562
+ status: "approved" as const,
563
+ user: {
564
+ login: user.login,
565
+ name: user.name,
566
+ avatar_url: user.avatar_url,
567
+ },
568
+ }
569
+ }
570
+ if (payload.error === "authorization_pending") {
571
+ return {
572
+ status: "pending" as const,
573
+ interval: payload.interval ?? 5,
574
+ }
575
+ }
576
+ if (payload.error === "slow_down") {
577
+ return {
578
+ status: "pending" as const,
579
+ interval: Math.max(payload.interval ?? 5, 10),
580
+ }
581
+ }
582
+ if (payload.error === "access_denied") {
583
+ return { status: "denied" as const }
584
+ }
585
+ if (payload.error === "expired_token") {
586
+ return { status: "expired" as const }
587
+ }
588
+ throw new Error(payload.error || "GitHub auth polling failed")
589
+ }
590
+
591
+ async function githubUser() {
592
+ const token = await githubToken()
593
+ if (!token) return
594
+ return GithubApi.getUser(token).catch(() => undefined)
595
+ }
596
+
597
+ async function githubImports() {
598
+ const imports = await MobileGithubRepo.list()
599
+ return new Map(imports.map((item) => [item.fullName.toLowerCase(), item] as const))
600
+ }
601
+
602
+ function slug(input: string) {
603
+ return input
604
+ .trim()
605
+ .toLowerCase()
606
+ .replace(/[^a-z0-9]+/g, "-")
607
+ .replace(/^-+/, "")
608
+ .replace(/-+$/, "")
609
+ }
610
+
611
+ function sessionSeed() {
612
+ return Math.random().toString(36).slice(2, 8)
613
+ }
614
+
615
+ function defaultPullRequestBody(session: Session.Info) {
616
+ return [
617
+ `Generated from mobile session \`${session.id}\`.`,
618
+ session.share?.url ? `Session share: ${session.share.url}` : "",
619
+ ]
620
+ .filter(Boolean)
621
+ .join("\n\n")
622
+ }
623
+
624
+ function toHeadersObject(headers: Headers) {
625
+ return Object.fromEntries(headers.entries())
626
+ }
627
+
628
+ async function createExecutionWorkspace(input: {
629
+ directory: string
630
+ branch?: string | null
631
+ target: z.infer<typeof MobileExecutionTarget>
632
+ }) {
633
+ if (input.target !== "container") return undefined
634
+ const runtimeInfo = await getContainerRuntimeInfo()
635
+ if (!runtimeInfo.available || !runtimeInfo.runtime) {
636
+ throw new Error(
637
+ "Container sandbox is unavailable. Check Docker or Podman and the Nikcli workspace image on the host.",
638
+ )
639
+ }
640
+ const runtime: "docker" | "podman" = runtimeInfo.runtime
641
+ const project = await Project.fromDirectory(input.directory)
642
+ return Workspace.create({
643
+ projectID: project.project.id,
644
+ branch: input.branch ?? null,
645
+ config: {
646
+ type: "container",
647
+ directory: input.directory,
648
+ runtime,
649
+ image: runtimeInfo.image,
650
+ containerName: "pending",
651
+ port: 1,
652
+ serverUrl: "http://127.0.0.1:1",
653
+ },
654
+ })
655
+ }
656
+
657
+ async function statusForSession(session: Session.Info) {
658
+ return Instance.provide({
659
+ directory: session.directory,
660
+ async fn() {
661
+ return SessionStatus.get(session.id)
662
+ },
663
+ })
664
+ }
665
+
666
+ export const MobileRoutes = lazy(() =>
667
+ new Hono()
668
+ .post(
669
+ "/auth/token",
670
+ describeRoute({
671
+ summary: "Create mobile auth token",
672
+ description: "Exchange valid Basic auth credentials for a long-lived mobile Bearer token.",
673
+ operationId: "mobile.auth.token.create",
674
+ responses: {
675
+ 200: {
676
+ description: "Mobile token",
677
+ content: {
678
+ "application/json": {
679
+ schema: resolver(z.object({ token: z.string(), info: MobileAuth.PublicToken })),
680
+ },
681
+ },
682
+ },
683
+ },
684
+ }),
685
+ validator(
686
+ "json",
687
+ z
688
+ .object({
689
+ name: z.string().optional(),
690
+ expiresInDays: z.number().optional(),
691
+ })
692
+ .optional(),
693
+ ),
694
+ async (c) => {
695
+ const body = c.req.valid("json")
696
+ const result = await MobileAuth.create(body ?? undefined)
697
+ return c.json(result)
698
+ },
699
+ )
700
+ .delete(
701
+ "/auth/token/:id",
702
+ describeRoute({
703
+ summary: "Revoke mobile auth token",
704
+ description: "Revoke a previously issued mobile Bearer token.",
705
+ operationId: "mobile.auth.token.revoke",
706
+ responses: {
707
+ 200: {
708
+ description: "Token revoked",
709
+ content: { "application/json": { schema: resolver(z.object({ revoked: z.boolean() })) } },
710
+ },
711
+ },
712
+ }),
713
+ validator("param", z.object({ id: z.string() })),
714
+ async (c) => {
715
+ const removed = await MobileAuth.remove(c.req.valid("param").id)
716
+ return c.json({ revoked: removed })
717
+ },
718
+ )
719
+ .get(
720
+ "/auth/token",
721
+ describeRoute({
722
+ summary: "List mobile auth tokens",
723
+ description: "List all active mobile Bearer tokens.",
724
+ operationId: "mobile.auth.token.list",
725
+ responses: {
726
+ 200: {
727
+ description: "Token list",
728
+ content: { "application/json": { schema: resolver(MobileAuth.PublicToken.array()) } },
729
+ },
730
+ },
731
+ }),
732
+ async (c) => {
733
+ return c.json(await MobileAuth.list())
734
+ },
735
+ )
736
+ .get(
737
+ "/bootstrap",
738
+ describeRoute({
739
+ summary: "Get mobile bootstrap payload",
740
+ description: "Return the current mobile bootstrap state for the connected host.",
741
+ operationId: "mobile.bootstrap",
742
+ responses: {
743
+ 200: {
744
+ description: "Bootstrap payload",
745
+ content: { "application/json": { schema: resolver(MobileBootstrap) } },
746
+ },
747
+ },
748
+ }),
749
+ async (c) => {
750
+ const projects = await Project.list()
751
+ const token = currentToken(c)
752
+ const user = await githubUser()
753
+ const container = await getContainerRuntimeInfo()
754
+ const oauth = await githubOAuthClientID()
755
+ return c.json({
756
+ version: Installation.VERSION,
757
+ auth: {
758
+ bearerEnabled: true,
759
+ currentToken: token,
760
+ },
761
+ currentProject: {
762
+ ...Instance.project,
763
+ current: true,
764
+ },
765
+ projects: projects.map((project) => ({
766
+ ...project,
767
+ current: project.id === Instance.project.id,
768
+ })),
769
+ execution: {
770
+ container,
771
+ },
772
+ github: {
773
+ connected: Boolean(user),
774
+ oauthDeviceEnabled: true,
775
+ oauthDeviceConfigured: Boolean(oauth.clientID),
776
+ oauthClientSource: oauth.source,
777
+ user: user
778
+ ? {
779
+ login: user.login,
780
+ name: user.name,
781
+ avatar_url: user.avatar_url,
782
+ }
783
+ : undefined,
784
+ },
785
+ })
786
+ },
787
+ )
788
+ .get(
789
+ "/memory/history",
790
+ describeRoute({
791
+ summary: "List prompt history for mobile",
792
+ description: "Return recent prompt history stored on the Nikcli host.",
793
+ operationId: "mobile.memory.history",
794
+ responses: {
795
+ 200: {
796
+ description: "Prompt history",
797
+ content: { "application/json": { schema: resolver(MobilePromptHistoryEntry.array()) } },
798
+ },
799
+ },
800
+ }),
801
+ async (c) => {
802
+ return c.json(await listPromptHistory())
803
+ },
804
+ )
805
+ .get(
806
+ "/memory/search",
807
+ describeRoute({
808
+ summary: "Search prompt memories for mobile",
809
+ description: "Search across stored session messages for memory-like prompt context from mobile.",
810
+ operationId: "mobile.memory.search",
811
+ responses: {
812
+ 200: {
813
+ description: "Memory search hits",
814
+ content: { "application/json": { schema: resolver(MobileMemorySearchHit.array()) } },
815
+ },
816
+ },
817
+ }),
818
+ validator("query", z.object({ query: z.string().trim().min(1) })),
819
+ async (c) => {
820
+ const query = c.req.valid("query").query
821
+ return c.json(await searchPromptMemories(query))
822
+ },
823
+ )
824
+ .get(
825
+ "/memory/stash",
826
+ describeRoute({
827
+ summary: "List prompt stash for mobile",
828
+ description: "Return reusable prompt snippets stored on the Nikcli host.",
829
+ operationId: "mobile.memory.stash.list",
830
+ responses: {
831
+ 200: {
832
+ description: "Prompt stash",
833
+ content: { "application/json": { schema: resolver(MobilePromptStashEntry.array()) } },
834
+ },
835
+ },
836
+ }),
837
+ async (c) => {
838
+ return c.json(await listPromptStash())
839
+ },
840
+ )
841
+ .post(
842
+ "/memory/stash",
843
+ describeRoute({
844
+ summary: "Create prompt stash entry",
845
+ description: "Save a reusable prompt snippet on the Nikcli host.",
846
+ operationId: "mobile.memory.stash.create",
847
+ responses: {
848
+ 200: {
849
+ description: "Created prompt stash entry",
850
+ content: { "application/json": { schema: resolver(MobilePromptStashEntry) } },
851
+ },
852
+ ...errors(400),
853
+ },
854
+ }),
855
+ validator("json", MobilePromptStashCreateInput),
856
+ async (c) => {
857
+ const body = c.req.valid("json")
858
+ const [entry] = (
859
+ await PromptStashStore.push({
860
+ input: body.input.trim(),
861
+ parts: [] as any,
862
+ })
863
+ ).slice(-1)
864
+ return c.json({
865
+ id: entry.id,
866
+ input: entry.input,
867
+ timestamp: entry.timestamp,
868
+ partsCount: 0,
869
+ })
870
+ },
871
+ )
872
+ .delete(
873
+ "/memory/stash/:id",
874
+ describeRoute({
875
+ summary: "Delete prompt stash entry",
876
+ description: "Remove a reusable prompt snippet from the Nikcli host.",
877
+ operationId: "mobile.memory.stash.delete",
878
+ responses: {
879
+ 200: {
880
+ description: "Deleted",
881
+ content: { "application/json": { schema: resolver(z.object({ success: z.literal(true) })) } },
882
+ },
883
+ ...errors(404),
884
+ },
885
+ }),
886
+ validator("param", z.object({ id: z.string() })),
887
+ async (c) => {
888
+ const id = c.req.valid("param").id
889
+ const current = await PromptStashStore.list()
890
+ const next = await PromptStashStore.removeByID(id)
891
+ if (next.length === current.length) {
892
+ return c.json({ error: "Prompt snippet not found" }, 404)
893
+ }
894
+ return c.json({ success: true as const })
895
+ },
896
+ )
897
+ .get(
898
+ "/command",
899
+ describeRoute({
900
+ summary: "List mobile commands",
901
+ description: "Return command metadata safe for the mobile command palette and slash autocomplete.",
902
+ operationId: "mobile.command.list",
903
+ responses: {
904
+ 200: {
905
+ description: "Commands",
906
+ content: { "application/json": { schema: resolver(MobileCommand.array()) } },
907
+ },
908
+ },
909
+ }),
910
+ async (c) => {
911
+ const commands = await Command.list()
912
+ return c.json(
913
+ commands
914
+ .map((command) => ({
915
+ name: command.name,
916
+ description: command.description,
917
+ agent: command.agent,
918
+ model: command.model,
919
+ mcp: command.mcp,
920
+ skill: command.skill,
921
+ subtask: command.subtask,
922
+ hints: command.hints,
923
+ }))
924
+ .sort((a, b) => a.name.localeCompare(b.name)),
925
+ )
926
+ },
927
+ )
928
+ .get(
929
+ "/project",
930
+ describeRoute({
931
+ summary: "List local projects for mobile",
932
+ description: "Return local projects and sandboxes visible to the connected Nikcli host.",
933
+ operationId: "mobile.project.list",
934
+ responses: {
935
+ 200: {
936
+ description: "Projects",
937
+ content: { "application/json": { schema: resolver(MobileProject.array()) } },
938
+ },
939
+ },
940
+ }),
941
+ async (c) => {
942
+ const projects = await Project.list()
943
+ return c.json(
944
+ projects.map((project) => ({
945
+ ...project,
946
+ current: project.id === Instance.project.id,
947
+ })),
948
+ )
949
+ },
950
+ )
951
+ .get(
952
+ "/github/repos",
953
+ describeRoute({
954
+ summary: "List GitHub repositories for mobile",
955
+ description: "List repositories available to the stored GitHub connector token.",
956
+ operationId: "mobile.github.repos",
957
+ responses: {
958
+ 200: {
959
+ description: "GitHub repositories",
960
+ content: { "application/json": { schema: resolver(z.array(z.any())) } },
961
+ },
962
+ ...errors(401),
963
+ },
964
+ }),
965
+ async (c) => {
966
+ const token = await githubToken()
967
+ if (!token) return c.json({ error: "GitHub token not configured" }, 401)
968
+ const [repos, imports] = await Promise.all([GithubApi.listRepos(token, "all", "updated"), githubImports()])
969
+ return c.json(
970
+ repos.map((repo: any) => {
971
+ const existing = imports.get(String(repo.full_name).toLowerCase())
972
+ return {
973
+ ...repo,
974
+ imported: Boolean(existing),
975
+ imported_directory: existing?.directory,
976
+ imported_project_id: existing?.projectID,
977
+ }
978
+ }),
979
+ )
980
+ },
981
+ )
982
+ .get(
983
+ "/github/repos/:owner/:repo/branches",
984
+ describeRoute({
985
+ summary: "List GitHub branches for mobile",
986
+ description: "List branches for a GitHub repository available to the stored mobile GitHub token.",
987
+ operationId: "mobile.github.branches",
988
+ responses: {
989
+ 200: {
990
+ description: "GitHub branches",
991
+ content: { "application/json": { schema: resolver(MobileGithubBranch.array()) } },
992
+ },
993
+ ...errors(401),
994
+ },
995
+ }),
996
+ validator("param", z.object({ owner: z.string(), repo: z.string() })),
997
+ async (c) => {
998
+ const token = await githubToken()
999
+ if (!token) return c.json({ error: "GitHub token not configured" }, 401)
1000
+ const params = c.req.valid("param")
1001
+ const branches = await GithubApi.listBranches(token, params.owner, params.repo)
1002
+ return c.json(branches)
1003
+ },
1004
+ )
1005
+ .get(
1006
+ "/github/imports",
1007
+ describeRoute({
1008
+ summary: "List imported GitHub repos for mobile",
1009
+ description: "List GitHub repositories that have already been cloned into the Nikcli host cache.",
1010
+ operationId: "mobile.github.imports",
1011
+ responses: {
1012
+ 200: {
1013
+ description: "Imported repos",
1014
+ content: { "application/json": { schema: resolver(MobileGithubRepo.Import.array()) } },
1015
+ },
1016
+ },
1017
+ }),
1018
+ async (c) => {
1019
+ return c.json(await MobileGithubRepo.list())
1020
+ },
1021
+ )
1022
+ .post(
1023
+ "/github/oauth/device",
1024
+ describeRoute({
1025
+ summary: "Start GitHub OAuth device flow",
1026
+ description: "Start a GitHub device authorization flow and return the verification code for mobile sign-in.",
1027
+ operationId: "mobile.github.oauth.device.start",
1028
+ responses: {
1029
+ 200: {
1030
+ description: "GitHub device flow started",
1031
+ content: { "application/json": { schema: resolver(MobileGithubDeviceAuthStart) } },
1032
+ },
1033
+ ...errors(400),
1034
+ },
1035
+ }),
1036
+ async (c) => {
1037
+ try {
1038
+ return c.json(await startGithubDeviceAuth())
1039
+ } catch (error) {
1040
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 400)
1041
+ }
1042
+ },
1043
+ )
1044
+ .post(
1045
+ "/github/oauth/device/poll",
1046
+ describeRoute({
1047
+ summary: "Poll GitHub OAuth device flow",
1048
+ description: "Poll GitHub device authorization status and persist the approved token on the host.",
1049
+ operationId: "mobile.github.oauth.device.poll",
1050
+ responses: {
1051
+ 200: {
1052
+ description: "GitHub device flow status",
1053
+ content: { "application/json": { schema: resolver(MobileGithubDeviceAuthPollResult) } },
1054
+ },
1055
+ ...errors(400),
1056
+ },
1057
+ }),
1058
+ validator("json", MobileGithubDeviceAuthPollInput),
1059
+ async (c) => {
1060
+ try {
1061
+ return c.json(await pollGithubDeviceAuth(c.req.valid("json").deviceCode))
1062
+ } catch (error) {
1063
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 400)
1064
+ }
1065
+ },
1066
+ )
1067
+ .post(
1068
+ "/github/auth",
1069
+ describeRoute({
1070
+ summary: "Store GitHub token for mobile",
1071
+ description: "Persist a GitHub token on the Nikcli host for mobile repo access.",
1072
+ operationId: "mobile.github.auth.set",
1073
+ responses: {
1074
+ 200: {
1075
+ description: "GitHub auth status",
1076
+ content: { "application/json": { schema: resolver(z.object({ success: z.literal(true) })) } },
1077
+ },
1078
+ ...errors(400),
1079
+ },
1080
+ }),
1081
+ validator("json", GithubAuthInput),
1082
+ async (c) => {
1083
+ const payload = c.req.valid("json")
1084
+ await ConnectorAuth.set("github", { token: payload.token })
1085
+ Connectors.invalidateConnector("github")
1086
+ return c.json({ success: true as const })
1087
+ },
1088
+ )
1089
+ .delete(
1090
+ "/github/auth",
1091
+ describeRoute({
1092
+ summary: "Remove stored GitHub token for mobile",
1093
+ description: "Delete the mobile GitHub token from the Nikcli host.",
1094
+ operationId: "mobile.github.auth.remove",
1095
+ responses: {
1096
+ 200: {
1097
+ description: "GitHub auth removed",
1098
+ content: { "application/json": { schema: resolver(z.object({ success: z.literal(true) })) } },
1099
+ },
1100
+ },
1101
+ }),
1102
+ async (c) => {
1103
+ await ConnectorAuth.remove("github")
1104
+ Connectors.invalidateConnector("github")
1105
+ return c.json({ success: true as const })
1106
+ },
1107
+ )
1108
+ .post(
1109
+ "/github/import",
1110
+ describeRoute({
1111
+ summary: "Import GitHub repo into Nikcli host",
1112
+ description: "Clone or refresh a repository from the connected GitHub account into the managed host cache.",
1113
+ operationId: "mobile.github.import",
1114
+ responses: {
1115
+ 200: {
1116
+ description: "Imported repository",
1117
+ content: {
1118
+ "application/json": {
1119
+ schema: resolver(
1120
+ z.object({
1121
+ import: MobileGithubRepo.Import,
1122
+ project: Project.Info,
1123
+ }),
1124
+ ),
1125
+ },
1126
+ },
1127
+ },
1128
+ ...errors(400, 401),
1129
+ },
1130
+ }),
1131
+ validator("json", MobileGithubRepo.ImportRequest),
1132
+ async (c) => {
1133
+ const token = await githubToken()
1134
+ if (!token) return c.json({ error: "GitHub token not configured" }, 401)
1135
+ const result = await MobileGithubRepo.importRepo(c.req.valid("json"), token)
1136
+ return c.json(result)
1137
+ },
1138
+ )
1139
+ .post(
1140
+ "/github/session",
1141
+ describeRoute({
1142
+ summary: "Create GitHub-backed mobile session",
1143
+ description:
1144
+ "Import a GitHub repo if needed, create an isolated worktree from a base branch, and start a session there.",
1145
+ operationId: "mobile.github.session.create",
1146
+ responses: {
1147
+ 200: {
1148
+ description: "GitHub mobile session",
1149
+ content: { "application/json": { schema: resolver(MobileGithubSessionCreateResult) } },
1150
+ },
1151
+ ...errors(400, 401),
1152
+ },
1153
+ }),
1154
+ validator("json", MobileGithubSessionCreateInput),
1155
+ async (c) => {
1156
+ const token = await githubToken()
1157
+ if (!token) return c.json({ error: "GitHub token not configured" }, 401)
1158
+
1159
+ const body = c.req.valid("json")
1160
+ const baseBranch = body.baseBranch.trim() || body.defaultBranch
1161
+ const imported = await MobileGithubRepo.importRepo(
1162
+ {
1163
+ owner: body.owner,
1164
+ repo: body.repo,
1165
+ cloneUrl: body.cloneUrl,
1166
+ defaultBranch: body.defaultBranch,
1167
+ private: body.private,
1168
+ },
1169
+ token,
1170
+ )
1171
+
1172
+ const seed = sessionSeed()
1173
+ const headBranch = `nikcli/mobile/${slug(body.repo)}/${seed}`
1174
+ let workspace: Workspace.Info | undefined
1175
+ const worktree = await Instance.provide({
1176
+ directory: imported.import.directory,
1177
+ async fn() {
1178
+ return Worktree.create({
1179
+ name: `${slug(body.repo)}-${slug(baseBranch)}-${seed}`,
1180
+ branch: headBranch,
1181
+ baseBranch,
1182
+ remote: "origin",
1183
+ })
1184
+ },
1185
+ })
1186
+
1187
+ try {
1188
+ workspace = await createExecutionWorkspace({
1189
+ directory: worktree.directory,
1190
+ branch: headBranch,
1191
+ target: body.executionTarget,
1192
+ })
1193
+
1194
+ const session = await Instance.provide({
1195
+ directory: worktree.directory,
1196
+ async fn() {
1197
+ return WorkspaceContext.provide({
1198
+ workspaceID: workspace?.id,
1199
+ async fn() {
1200
+ return Session.create({
1201
+ title: body.title?.trim() || `${body.owner}/${body.repo} ${baseBranch}`,
1202
+ workspaceID: workspace?.id,
1203
+ github: {
1204
+ owner: body.owner,
1205
+ repo: body.repo,
1206
+ fullName: `${body.owner}/${body.repo}`,
1207
+ baseBranch,
1208
+ headBranch,
1209
+ repositoryDirectory: imported.import.directory,
1210
+ cloneUrl: body.cloneUrl,
1211
+ htmlUrl: body.htmlUrl,
1212
+ private: body.private,
1213
+ worktree,
1214
+ },
1215
+ })
1216
+ },
1217
+ })
1218
+ },
1219
+ })
1220
+
1221
+ return c.json({ session, worktree, project: imported.project, workspace })
1222
+ } catch (error) {
1223
+ if (workspace) {
1224
+ await Workspace.remove(workspace.id).catch(() => undefined)
1225
+ }
1226
+ await Instance.provide({
1227
+ directory: imported.import.directory,
1228
+ async fn() {
1229
+ await Worktree.remove({ directory: worktree.directory }).catch(() => undefined)
1230
+ },
1231
+ }).catch(() => undefined)
1232
+ throw error
1233
+ }
1234
+ },
1235
+ )
1236
+ .get(
1237
+ "/session",
1238
+ describeRoute({
1239
+ summary: "List mobile sessions",
1240
+ description: "Return mobile-friendly session summaries with current status.",
1241
+ operationId: "mobile.session.list",
1242
+ responses: {
1243
+ 200: {
1244
+ description: "Sessions",
1245
+ content: { "application/json": { schema: resolver(MobileSessionSummary.array()) } },
1246
+ },
1247
+ },
1248
+ }),
1249
+ validator(
1250
+ "query",
1251
+ z.object({
1252
+ limit: z.coerce.number().optional(),
1253
+ search: z.string().optional(),
1254
+ }),
1255
+ ),
1256
+ async (c) => {
1257
+ const query = c.req.valid("query")
1258
+ const term = query.search?.toLowerCase()
1259
+ const sessions: z.infer<typeof MobileSessionSummary>[] = []
1260
+ // List sessions across all projects for mobile (cross-project view)
1261
+ const allKeys = await Storage.list(["session"])
1262
+ const seen = new Set<string>()
1263
+ for (const key of allKeys) {
1264
+ if (key.length !== 3) continue
1265
+ const sessionID = key[2]
1266
+ if (seen.has(sessionID)) continue
1267
+ seen.add(sessionID)
1268
+ try {
1269
+ const session = await Storage.read<Session.Info>(key)
1270
+ if (term) {
1271
+ const haystack = [
1272
+ session.title,
1273
+ session.github?.fullName,
1274
+ session.github?.baseBranch,
1275
+ session.github?.headBranch,
1276
+ ]
1277
+ .filter(Boolean)
1278
+ .join(" ")
1279
+ .toLowerCase()
1280
+ if (!haystack.includes(term)) continue
1281
+ }
1282
+ sessions.push({ info: session, status: await statusForSession(session) })
1283
+ } catch {
1284
+ continue
1285
+ }
1286
+ }
1287
+ // Sort by most recently updated, then apply limit
1288
+ sessions.sort((a, b) => b.info.time.updated - a.info.time.updated)
1289
+ return c.json(query.limit ? sessions.slice(0, query.limit) : sessions)
1290
+ },
1291
+ )
1292
+ .post(
1293
+ "/session",
1294
+ describeRoute({
1295
+ summary: "Create mobile session",
1296
+ description: "Create a new session for the mobile app.",
1297
+ operationId: "mobile.session.create",
1298
+ responses: {
1299
+ 200: {
1300
+ description: "Created session",
1301
+ content: { "application/json": { schema: resolver(Session.Info) } },
1302
+ },
1303
+ ...errors(400),
1304
+ },
1305
+ }),
1306
+ validator("json", MobileSessionCreateInput),
1307
+ async (c) => {
1308
+ const body = c.req.valid("json") as Record<string, unknown> | undefined
1309
+ const executionTarget = body?.executionTarget === "container" ? "container" : "local"
1310
+ let workspace: Workspace.Info | undefined
1311
+ const sessionInput = body
1312
+ ? {
1313
+ parentID: typeof body.parentID === "string" ? body.parentID : undefined,
1314
+ title: typeof body.title === "string" ? body.title : undefined,
1315
+ permission: body.permission as Session.Info["permission"],
1316
+ github: body.github as Session.Info["github"],
1317
+ }
1318
+ : undefined
1319
+
1320
+ try {
1321
+ workspace = await createExecutionWorkspace({
1322
+ directory: Instance.directory,
1323
+ target: executionTarget,
1324
+ })
1325
+ const session = await WorkspaceContext.provide({
1326
+ workspaceID: workspace?.id,
1327
+ async fn() {
1328
+ return Session.create(
1329
+ workspace?.id
1330
+ ? {
1331
+ ...sessionInput,
1332
+ workspaceID: workspace.id,
1333
+ }
1334
+ : sessionInput,
1335
+ )
1336
+ },
1337
+ })
1338
+ return c.json(session)
1339
+ } catch (error) {
1340
+ if (workspace) {
1341
+ await Workspace.remove(workspace.id).catch(() => undefined)
1342
+ }
1343
+ throw error
1344
+ }
1345
+ },
1346
+ )
1347
+ .get(
1348
+ "/session/:sessionID",
1349
+ describeRoute({
1350
+ summary: "Get mobile session detail",
1351
+ description: "Return a session, its messages, status, and pending permissions.",
1352
+ operationId: "mobile.session.detail",
1353
+ responses: {
1354
+ 200: {
1355
+ description: "Session detail",
1356
+ content: { "application/json": { schema: resolver(MobileSessionDetail) } },
1357
+ },
1358
+ ...errors(404),
1359
+ },
1360
+ }),
1361
+ validator("param", z.object({ sessionID: z.string() })),
1362
+ async (c) => {
1363
+ const sessionID = c.req.valid("param").sessionID
1364
+ const info = await Session.getAnyProject(sessionID)
1365
+ const { messages, permissions, status } = await Instance.provide({
1366
+ directory: info.directory,
1367
+ async fn() {
1368
+ const [messages, permissions] = await Promise.all([
1369
+ Session.messages({ sessionID }),
1370
+ PermissionNext.list().then((items) => items.filter((item) => item.sessionID === sessionID)),
1371
+ ])
1372
+ return { messages, permissions, status: SessionStatus.get(sessionID) }
1373
+ },
1374
+ })
1375
+ return c.json({
1376
+ info,
1377
+ status,
1378
+ messages,
1379
+ permissions,
1380
+ })
1381
+ },
1382
+ )
1383
+ .get(
1384
+ "/session/:sessionID/diff/:messageID",
1385
+ describeRoute({
1386
+ summary: "Get session diff for mobile",
1387
+ description: "Return file diffs for a specific message in a session.",
1388
+ operationId: "mobile.session.diff",
1389
+ responses: {
1390
+ 200: {
1391
+ description: "Message diff",
1392
+ content: { "application/json": { schema: resolver(Snapshot.FileDiff.array()) } },
1393
+ },
1394
+ },
1395
+ }),
1396
+ validator("param", z.object({ sessionID: z.string(), messageID: z.string() })),
1397
+ async (c) => {
1398
+ const params = c.req.valid("param")
1399
+ const session = await Session.getAnyProject(params.sessionID)
1400
+ const result = await Instance.provide({
1401
+ directory: session.directory,
1402
+ async fn() {
1403
+ return SessionSummary.diff({ sessionID: params.sessionID, messageID: params.messageID })
1404
+ },
1405
+ })
1406
+ return c.json(result)
1407
+ },
1408
+ )
1409
+ .get(
1410
+ "/session/:sessionID/command",
1411
+ describeRoute({
1412
+ summary: "List session commands for mobile",
1413
+ description: "Return command metadata resolved in the current session context for mobile slash autocomplete.",
1414
+ operationId: "mobile.session.command.list",
1415
+ responses: {
1416
+ 200: {
1417
+ description: "Commands",
1418
+ content: { "application/json": { schema: resolver(MobileCommand.array()) } },
1419
+ },
1420
+ ...errors(404),
1421
+ },
1422
+ }),
1423
+ validator("param", z.object({ sessionID: z.string() })),
1424
+ async (c) => {
1425
+ const sessionID = c.req.valid("param").sessionID
1426
+ const session = await Session.getAnyProject(sessionID)
1427
+ if (session.workspaceID) {
1428
+ const response = await proxyWorkspaceRequest({
1429
+ workspaceID: session.workspaceID,
1430
+ method: "GET",
1431
+ url: "/command",
1432
+ signal: c.req.raw.signal,
1433
+ })
1434
+
1435
+ if (response) {
1436
+ if (!response.ok) {
1437
+ return new Response(response.body, {
1438
+ status: response.status,
1439
+ headers: toHeadersObject(response.headers),
1440
+ })
1441
+ }
1442
+
1443
+ const commands = (await response.json().catch(() => [])) as Array<Record<string, unknown>>
1444
+ return c.json(
1445
+ commands
1446
+ .map((command) => ({
1447
+ name: typeof command.name === "string" ? command.name : "unknown",
1448
+ description: typeof command.description === "string" ? command.description : undefined,
1449
+ agent: typeof command.agent === "string" ? command.agent : undefined,
1450
+ model: typeof command.model === "string" ? command.model : undefined,
1451
+ mcp: typeof command.mcp === "boolean" ? command.mcp : undefined,
1452
+ skill: typeof command.skill === "boolean" ? command.skill : undefined,
1453
+ subtask: typeof command.subtask === "boolean" ? command.subtask : undefined,
1454
+ hints: Array.isArray(command.hints)
1455
+ ? command.hints.filter((hint): hint is string => typeof hint === "string")
1456
+ : [],
1457
+ }))
1458
+ .filter((command) => command.name !== "unknown")
1459
+ .sort((a, b) => a.name.localeCompare(b.name)),
1460
+ )
1461
+ }
1462
+ }
1463
+
1464
+ const commands = await Instance.provide({
1465
+ directory: session.directory,
1466
+ async fn() {
1467
+ return WorkspaceContext.provide({
1468
+ workspaceID: session.workspaceID,
1469
+ async fn() {
1470
+ return Command.list()
1471
+ },
1472
+ })
1473
+ },
1474
+ })
1475
+ return c.json(
1476
+ commands
1477
+ .map((command) => ({
1478
+ name: command.name,
1479
+ description: command.description,
1480
+ agent: command.agent,
1481
+ model: command.model,
1482
+ mcp: command.mcp,
1483
+ skill: command.skill,
1484
+ subtask: command.subtask,
1485
+ hints: command.hints,
1486
+ }))
1487
+ .sort((a, b) => a.name.localeCompare(b.name)),
1488
+ )
1489
+ },
1490
+ )
1491
+ .post(
1492
+ "/session/:sessionID/message",
1493
+ describeRoute({
1494
+ summary: "Send mobile session message",
1495
+ description: "Queue a message for a session and rely on the session stream for realtime updates.",
1496
+ operationId: "mobile.session.message",
1497
+ responses: {
1498
+ 202: {
1499
+ description: "Message accepted",
1500
+ content: { "application/json": { schema: resolver(z.object({ accepted: z.literal(true) })) } },
1501
+ },
1502
+ ...errors(400, 404),
1503
+ },
1504
+ }),
1505
+ validator("param", z.object({ sessionID: z.string() })),
1506
+ validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
1507
+ async (c) => {
1508
+ const sessionID = c.req.valid("param").sessionID
1509
+ const body = c.req.valid("json")
1510
+ const session = await Session.getAnyProject(sessionID)
1511
+ if (session.github?.worktree.cleanedAt) {
1512
+ return c.json({ error: "Session worktree has been cleaned up" }, 400)
1513
+ }
1514
+
1515
+ const defaults = !body.agent || !body.model ? await resolveMobilePromptDefaults(session) : undefined
1516
+ const promptBody = {
1517
+ ...body,
1518
+ agent: body.agent ?? defaults?.agent,
1519
+ model: body.model ?? defaults?.model,
1520
+ }
1521
+
1522
+ if (session.workspaceID) {
1523
+ const response = await proxyWorkspaceRequest({
1524
+ workspaceID: session.workspaceID,
1525
+ method: "POST",
1526
+ url: `/session/${encodeURIComponent(sessionID)}/prompt_async`,
1527
+ body: JSON.stringify(promptBody),
1528
+ headers: {
1529
+ "content-type": "application/json",
1530
+ },
1531
+ signal: c.req.raw.signal,
1532
+ })
1533
+
1534
+ if (response) {
1535
+ if (!response.ok) {
1536
+ return new Response(response.body, {
1537
+ status: response.status,
1538
+ headers: toHeadersObject(response.headers),
1539
+ })
1540
+ }
1541
+
1542
+ return c.json({ accepted: true as const }, 202)
1543
+ }
1544
+ }
1545
+
1546
+ void Instance.provide({
1547
+ directory: session.directory,
1548
+ async fn() {
1549
+ return SessionPrompt.prompt({
1550
+ ...promptBody,
1551
+ sessionID,
1552
+ })
1553
+ },
1554
+ }).catch((error) => {
1555
+ const message = error instanceof Error ? error.message : String(error)
1556
+ SessionStatus.set(sessionID, { type: "idle" })
1557
+ Bus.publish(Session.Event.Error, {
1558
+ sessionID,
1559
+ error: new NamedError.Unknown({ message }).toObject(),
1560
+ })
1561
+ log.error("mobile session prompt failed", {
1562
+ sessionID,
1563
+ error: message,
1564
+ })
1565
+ })
1566
+ return c.json({ accepted: true as const }, 202)
1567
+ },
1568
+ )
1569
+ .post(
1570
+ "/session/:sessionID/command",
1571
+ describeRoute({
1572
+ summary: "Run mobile session command",
1573
+ description: "Execute a slash-style command against the current session.",
1574
+ operationId: "mobile.session.command",
1575
+ responses: {
1576
+ 200: {
1577
+ description: "Command result",
1578
+ content: { "application/json": { schema: resolver(MessageV2.WithParts) } },
1579
+ },
1580
+ ...errors(400, 404),
1581
+ },
1582
+ }),
1583
+ validator("param", z.object({ sessionID: z.string() })),
1584
+ validator("json", MobileSessionCommandInput),
1585
+ async (c) => {
1586
+ const sessionID = c.req.valid("param").sessionID
1587
+ const body = c.req.valid("json")
1588
+ const session = await Session.getAnyProject(sessionID)
1589
+ if (session.github?.worktree.cleanedAt) {
1590
+ return c.json({ error: "Session worktree has been cleaned up" }, 400)
1591
+ }
1592
+
1593
+ const commandBody = {
1594
+ command: body.command,
1595
+ arguments: body.arguments,
1596
+ agent: body.agent,
1597
+ model: body.model ? `${body.model.providerID}/${body.model.modelID}` : undefined,
1598
+ }
1599
+
1600
+ if (session.workspaceID) {
1601
+ const response = await proxyWorkspaceRequest({
1602
+ workspaceID: session.workspaceID,
1603
+ method: "POST",
1604
+ url: `/session/${encodeURIComponent(sessionID)}/command`,
1605
+ body: JSON.stringify(commandBody),
1606
+ headers: {
1607
+ "content-type": "application/json",
1608
+ },
1609
+ signal: c.req.raw.signal,
1610
+ })
1611
+
1612
+ if (response) {
1613
+ return new Response(response.body, {
1614
+ status: response.status,
1615
+ headers: toHeadersObject(response.headers),
1616
+ })
1617
+ }
1618
+ }
1619
+
1620
+ const result = await Instance.provide({
1621
+ directory: session.directory,
1622
+ async fn() {
1623
+ return SessionPrompt.command({
1624
+ ...commandBody,
1625
+ sessionID,
1626
+ })
1627
+ },
1628
+ })
1629
+
1630
+ return c.json(result)
1631
+ },
1632
+ )
1633
+ .post(
1634
+ "/session/:sessionID/abort",
1635
+ describeRoute({
1636
+ summary: "Abort mobile session",
1637
+ description: "Abort the active run for a session.",
1638
+ operationId: "mobile.session.abort",
1639
+ responses: {
1640
+ 200: {
1641
+ description: "Session aborted",
1642
+ content: { "application/json": { schema: resolver(z.object({ success: z.literal(true) })) } },
1643
+ },
1644
+ },
1645
+ }),
1646
+ validator("param", z.object({ sessionID: z.string() })),
1647
+ async (c) => {
1648
+ const sessionID = c.req.valid("param").sessionID
1649
+ const session = await Session.getAnyProject(sessionID)
1650
+ if (session.workspaceID) {
1651
+ const response = await proxyWorkspaceRequest({
1652
+ workspaceID: session.workspaceID,
1653
+ method: "POST",
1654
+ url: `/session/${encodeURIComponent(sessionID)}/abort`,
1655
+ signal: c.req.raw.signal,
1656
+ })
1657
+
1658
+ if (response) {
1659
+ if (!response.ok) {
1660
+ return new Response(response.body, {
1661
+ status: response.status,
1662
+ headers: toHeadersObject(response.headers),
1663
+ })
1664
+ }
1665
+
1666
+ return c.json({ success: true as const })
1667
+ }
1668
+ }
1669
+ SessionPrompt.cancel(sessionID)
1670
+ return c.json({ success: true as const })
1671
+ },
1672
+ )
1673
+ .post(
1674
+ "/session/:sessionID/permissions/:permissionID",
1675
+ describeRoute({
1676
+ summary: "Respond to permission from mobile",
1677
+ description: "Approve, always approve, or reject a pending permission request.",
1678
+ operationId: "mobile.permission.respond",
1679
+ responses: {
1680
+ 200: {
1681
+ description: "Permission processed",
1682
+ content: { "application/json": { schema: resolver(z.object({ success: z.literal(true) })) } },
1683
+ },
1684
+ },
1685
+ }),
1686
+ validator("param", z.object({ sessionID: z.string(), permissionID: z.string() })),
1687
+ validator("json", z.object({ response: PermissionNext.Reply })),
1688
+ async (c) => {
1689
+ const params = c.req.valid("param")
1690
+ const session = await Session.getAnyProject(params.sessionID)
1691
+ if (session.workspaceID) {
1692
+ const response = await proxyWorkspaceRequest({
1693
+ workspaceID: session.workspaceID,
1694
+ method: "POST",
1695
+ url: `/session/${encodeURIComponent(params.sessionID)}/permissions/${encodeURIComponent(params.permissionID)}`,
1696
+ body: JSON.stringify({ response: c.req.valid("json").response }),
1697
+ headers: {
1698
+ "content-type": "application/json",
1699
+ },
1700
+ signal: c.req.raw.signal,
1701
+ })
1702
+
1703
+ if (response) {
1704
+ if (!response.ok) {
1705
+ return new Response(response.body, {
1706
+ status: response.status,
1707
+ headers: toHeadersObject(response.headers),
1708
+ })
1709
+ }
1710
+
1711
+ return c.json({ success: true as const })
1712
+ }
1713
+ }
1714
+ PermissionNext.reply({ requestID: params.permissionID, reply: c.req.valid("json").response })
1715
+ return c.json({ success: true as const })
1716
+ },
1717
+ )
1718
+ .post(
1719
+ "/session/:sessionID/publish",
1720
+ describeRoute({
1721
+ summary: "Publish GitHub session",
1722
+ description: "Commit the current worktree, push the session branch, and create or reuse a pull request.",
1723
+ operationId: "mobile.github.session.publish",
1724
+ responses: {
1725
+ 200: {
1726
+ description: "Published pull request",
1727
+ content: { "application/json": { schema: resolver(MobileGithubPublishResult) } },
1728
+ },
1729
+ ...errors(400, 401, 404),
1730
+ },
1731
+ }),
1732
+ validator("param", z.object({ sessionID: z.string() })),
1733
+ validator("json", MobileGithubPublishInput),
1734
+ async (c) => {
1735
+ const token = await githubToken()
1736
+ if (!token) return c.json({ error: "GitHub token not configured" }, 401)
1737
+
1738
+ const body = c.req.valid("json") ?? {}
1739
+ const sessionInfo = await Session.getAnyProject(c.req.valid("param").sessionID)
1740
+ if (!sessionInfo.github) return c.json({ error: "Session is not linked to GitHub" }, 400)
1741
+ if (sessionInfo.github.worktree.cleanedAt)
1742
+ return c.json({ error: "Session worktree has already been cleaned" }, 400)
1743
+
1744
+ return Instance.provide({
1745
+ directory: sessionInfo.directory,
1746
+ async fn() {
1747
+ const session = await Session.get(sessionInfo.id)
1748
+ const github = session.github
1749
+ if (!github) return c.json({ error: "Session is not linked to GitHub" }, 400)
1750
+ if (SessionStatus.get(session.id).type !== "idle") {
1751
+ return c.json({ error: "Wait for the session to become idle before publishing" }, 400)
1752
+ }
1753
+
1754
+ await MobileGithubRepo.runGit(["fetch", "origin", github.baseBranch, "--prune"], {
1755
+ cwd: session.directory,
1756
+ token,
1757
+ })
1758
+
1759
+ const dirty = await MobileGithubRepo.runGit(["status", "--porcelain"], {
1760
+ cwd: session.directory,
1761
+ token,
1762
+ })
1763
+
1764
+ if (dirty.trim()) {
1765
+ await MobileGithubRepo.runGit(["add", "-A"], { cwd: session.directory, token })
1766
+ await MobileGithubRepo.runGit(
1767
+ ["commit", "-m", body.commitMessage?.trim() || session.title.trim() || `Update ${github.fullName}`],
1768
+ {
1769
+ cwd: session.directory,
1770
+ token,
1771
+ },
1772
+ )
1773
+ }
1774
+
1775
+ await MobileGithubRepo.runGit(["push", "--set-upstream", "origin", github.headBranch], {
1776
+ cwd: session.directory,
1777
+ token,
1778
+ })
1779
+
1780
+ const ahead = await MobileGithubRepo.runGit(
1781
+ ["rev-list", "--left-right", "--count", `origin/${github.baseBranch}...HEAD`],
1782
+ {
1783
+ cwd: session.directory,
1784
+ token,
1785
+ },
1786
+ )
1787
+ const [, aheadCountText = "0"] = ahead.trim().split(/\s+/)
1788
+ const aheadCount = Number.parseInt(aheadCountText, 10) || 0
1789
+
1790
+ const commitSha = await MobileGithubRepo.runGit(["rev-parse", "HEAD"], {
1791
+ cwd: session.directory,
1792
+ token,
1793
+ })
1794
+
1795
+ const existingPullRequest =
1796
+ github.pullRequest ||
1797
+ (await GithubApi.findPullRequestByHead(
1798
+ token,
1799
+ github.owner,
1800
+ github.repo,
1801
+ `${github.owner}:${github.headBranch}`,
1802
+ )
1803
+ .then((value) =>
1804
+ value
1805
+ ? {
1806
+ number: value.number,
1807
+ url: value.html_url,
1808
+ title: value.title,
1809
+ }
1810
+ : undefined,
1811
+ )
1812
+ .catch(() => undefined))
1813
+
1814
+ const pullRequest =
1815
+ existingPullRequest ||
1816
+ (aheadCount > 0
1817
+ ? await GithubApi.createPullRequest(
1818
+ token,
1819
+ github.owner,
1820
+ github.repo,
1821
+ body.title?.trim() || session.title.trim() || `${github.fullName} changes`,
1822
+ github.headBranch,
1823
+ github.baseBranch,
1824
+ body.body?.trim() || defaultPullRequestBody(session),
1825
+ ).then((value) => ({
1826
+ number: value.number,
1827
+ url: value.html_url,
1828
+ title: value.title,
1829
+ }))
1830
+ : undefined)
1831
+
1832
+ if (!pullRequest) {
1833
+ return c.json({ error: "Create changes in the worktree before publishing a pull request" }, 400)
1834
+ }
1835
+
1836
+ await Session.update(session.id, (draft) => {
1837
+ if (!draft.github) return
1838
+ draft.github.pullRequest = pullRequest
1839
+ draft.github.lastCommitSha = commitSha.trim()
1840
+ draft.github.publishedAt = Date.now()
1841
+ draft.github.publishError = undefined
1842
+ })
1843
+
1844
+ return c.json({
1845
+ commitSha: commitSha.trim(),
1846
+ branch: github.headBranch,
1847
+ pullRequest,
1848
+ })
1849
+ },
1850
+ })
1851
+ },
1852
+ )
1853
+ .post(
1854
+ "/session/:sessionID/cleanup",
1855
+ describeRoute({
1856
+ summary: "Cleanup GitHub session worktree",
1857
+ description: "Remove the isolated worktree created for a GitHub-backed mobile session.",
1858
+ operationId: "mobile.github.session.cleanup",
1859
+ responses: {
1860
+ 200: {
1861
+ description: "Worktree cleaned",
1862
+ content: {
1863
+ "application/json": { schema: resolver(z.object({ success: z.literal(true) })) },
1864
+ },
1865
+ },
1866
+ ...errors(400, 404),
1867
+ },
1868
+ }),
1869
+ validator("param", z.object({ sessionID: z.string() })),
1870
+ async (c) => {
1871
+ const sessionInfo = await Session.getAnyProject(c.req.valid("param").sessionID)
1872
+ if (!sessionInfo.github) return c.json({ error: "Session is not linked to GitHub" }, 400)
1873
+ if (sessionInfo.github.worktree.cleanedAt) return c.json({ success: true as const })
1874
+
1875
+ const repositoryDirectory = sessionInfo.github.repositoryDirectory || sessionInfo.github.worktree.directory
1876
+ const idle = await Instance.provide({
1877
+ directory: sessionInfo.directory,
1878
+ async fn() {
1879
+ return SessionStatus.get(sessionInfo.id).type === "idle"
1880
+ },
1881
+ })
1882
+ if (!idle) {
1883
+ return c.json({ error: "Wait for the session to become idle before cleaning up the worktree" }, 400)
1884
+ }
1885
+
1886
+ await Instance.provide({
1887
+ directory: repositoryDirectory,
1888
+ async fn() {
1889
+ if (sessionInfo.workspaceID) {
1890
+ await Workspace.remove(sessionInfo.workspaceID).catch(() => undefined)
1891
+ }
1892
+ await Worktree.remove({ directory: sessionInfo.github!.worktree.directory })
1893
+ },
1894
+ })
1895
+
1896
+ await Instance.provide({
1897
+ directory: repositoryDirectory,
1898
+ async fn() {
1899
+ await Session.update(sessionInfo.id, (draft) => {
1900
+ if (!draft.github) return
1901
+ draft.github.worktree.cleanedAt = Date.now()
1902
+ })
1903
+ },
1904
+ })
1905
+
1906
+ return c.json({ success: true as const })
1907
+ },
1908
+ )
1909
+ .get(
1910
+ "/session/:sessionID/stream",
1911
+ describeRoute({
1912
+ summary: "Stream mobile session events",
1913
+ description: "Subscribe to session-scoped realtime events for the mobile chat UI.",
1914
+ operationId: "mobile.session.stream",
1915
+ responses: {
1916
+ 200: {
1917
+ description: "Session event stream",
1918
+ content: { "text/event-stream": { schema: resolver(z.any()) } },
1919
+ },
1920
+ },
1921
+ }),
1922
+ validator("param", z.object({ sessionID: z.string() })),
1923
+ async (c) => {
1924
+ const sessionID = c.req.valid("param").sessionID
1925
+ return streamSSE(c, async (stream) => {
1926
+ await stream.writeSSE({
1927
+ data: JSON.stringify({ type: "server.connected", properties: { sessionID } }),
1928
+ })
1929
+
1930
+ const onEvent = async (event: any) => {
1931
+ const payload = event?.payload
1932
+ if (!payload?.type) return
1933
+ const ids = extractSessionIDs(payload.properties)
1934
+ if (!ids.includes(sessionID)) return
1935
+ await stream.writeSSE({
1936
+ data: JSON.stringify(payload),
1937
+ })
1938
+ }
1939
+
1940
+ GlobalBus.on("event", onEvent)
1941
+
1942
+ const heartbeat = setInterval(() => {
1943
+ void stream.writeSSE({
1944
+ data: JSON.stringify({ type: "server.heartbeat", properties: { sessionID } }),
1945
+ })
1946
+ }, 30000)
1947
+
1948
+ await new Promise<void>((resolve) => {
1949
+ stream.onAbort(() => {
1950
+ clearInterval(heartbeat)
1951
+ GlobalBus.off("event", onEvent)
1952
+ resolve()
1953
+ })
1954
+ })
1955
+ })
1956
+ },
1957
+ )
1958
+ .post(
1959
+ "/worktree",
1960
+ describeRoute({
1961
+ summary: "Create mobile worktree",
1962
+ description: "Create a git worktree for sandboxed mobile work.",
1963
+ operationId: "mobile.worktree.create",
1964
+ responses: {
1965
+ 200: {
1966
+ description: "Worktree created",
1967
+ content: { "application/json": { schema: resolver(Worktree.Info) } },
1968
+ },
1969
+ ...errors(400),
1970
+ },
1971
+ }),
1972
+ validator("json", Worktree.CreateInput.optional()),
1973
+ async (c) => {
1974
+ const worktree = await Worktree.create(c.req.valid("json") ?? undefined)
1975
+ return c.json(worktree)
1976
+ },
1977
+ )
1978
+ .post(
1979
+ "/worktree/reset",
1980
+ describeRoute({
1981
+ summary: "Reset mobile worktree",
1982
+ description: "Reset a worktree back to the default branch state.",
1983
+ operationId: "mobile.worktree.reset",
1984
+ responses: {
1985
+ 200: {
1986
+ description: "Worktree reset",
1987
+ content: { "application/json": { schema: resolver(z.object({ success: z.literal(true) })) } },
1988
+ },
1989
+ },
1990
+ }),
1991
+ validator("json", Worktree.ResetInput),
1992
+ async (c) => {
1993
+ await Worktree.reset(c.req.valid("json"))
1994
+ return c.json({ success: true as const })
1995
+ },
1996
+ )
1997
+ .post(
1998
+ "/session/:sessionID/rename",
1999
+ describeRoute({
2000
+ summary: "Rename a session",
2001
+ description: "Update the title of an existing session.",
2002
+ operationId: "mobile.session.rename",
2003
+ responses: {
2004
+ 200: {
2005
+ description: "Session renamed",
2006
+ content: { "application/json": { schema: resolver(z.object({ success: z.literal(true) })) } },
2007
+ },
2008
+ },
2009
+ }),
2010
+ validator("param", z.object({ sessionID: z.string() })),
2011
+ validator("json", z.object({ title: z.string().min(1) })),
2012
+ async (c) => {
2013
+ const { sessionID } = c.req.valid("param")
2014
+ const { title } = c.req.valid("json")
2015
+ const session = await Session.get(sessionID)
2016
+ if (!session) return c.json({ error: "not found" }, 404)
2017
+ await Session.update(sessionID, (draft) => {
2018
+ draft.title = title.trim()
2019
+ })
2020
+ return c.json({ success: true as const })
2021
+ },
2022
+ )
2023
+ .delete(
2024
+ "/worktree",
2025
+ describeRoute({
2026
+ summary: "Remove mobile worktree",
2027
+ description: "Remove an existing worktree sandbox.",
2028
+ operationId: "mobile.worktree.remove",
2029
+ responses: {
2030
+ 200: {
2031
+ description: "Worktree removed",
2032
+ content: { "application/json": { schema: resolver(z.object({ success: z.literal(true) })) } },
2033
+ },
2034
+ },
2035
+ }),
2036
+ validator("json", Worktree.RemoveInput),
2037
+ async (c) => {
2038
+ const input = c.req.valid("json")
2039
+ await Worktree.remove(input)
2040
+ await Project.removeSandbox(Instance.project.id, input.directory).catch(() => undefined)
2041
+ return c.json({ success: true as const })
2042
+ },
2043
+ ),
2044
+ )