@quickchatai/quickreview 1.1.0 → 1.3.1

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 (471) hide show
  1. package/README.md +137 -57
  2. package/bin/qcr.js +1 -1
  3. package/config.example.toml +60 -14
  4. package/deploy/ansible/README.md +20 -12
  5. package/deploy/ansible/inventory.example.ini +1 -1
  6. package/deploy/collect-secrets.sh +1 -1
  7. package/deploy/qcr-update.sh +1 -1
  8. package/dist/extensions/cli/home.js +14 -0
  9. package/dist/extensions/cli/home.js.map +1 -0
  10. package/dist/extensions/cli/login-route.js +5 -0
  11. package/dist/extensions/cli/login-route.js.map +1 -0
  12. package/dist/extensions/cli/selfimprove.js +18 -0
  13. package/dist/extensions/cli/selfimprove.js.map +1 -0
  14. package/dist/extensions/home.js +350 -0
  15. package/dist/extensions/home.js.map +1 -0
  16. package/dist/extensions/login-route.js +180 -0
  17. package/dist/extensions/login-route.js.map +1 -0
  18. package/dist/extensions/onboarding.js +35 -11
  19. package/dist/extensions/onboarding.js.map +1 -1
  20. package/dist/extensions/selfimprove.js +49 -0
  21. package/dist/extensions/selfimprove.js.map +1 -0
  22. package/dist/extensions/setup-scan.js +23 -592
  23. package/dist/extensions/setup-scan.js.map +1 -1
  24. package/dist/extensions/setup.js +2 -47
  25. package/dist/extensions/setup.js.map +1 -1
  26. package/dist/src/archive/restore.js +18 -15
  27. package/dist/src/archive/restore.js.map +1 -1
  28. package/dist/src/botworker.js +13 -2
  29. package/dist/src/botworker.js.map +1 -1
  30. package/dist/src/channelfs.js +3 -26
  31. package/dist/src/channelfs.js.map +1 -1
  32. package/dist/src/ciretry.js +2 -1
  33. package/dist/src/ciretry.js.map +1 -1
  34. package/dist/src/cli.js +54 -61
  35. package/dist/src/cli.js.map +1 -1
  36. package/dist/src/cliops.js +3 -2
  37. package/dist/src/cliops.js.map +1 -1
  38. package/dist/src/commands.js +25 -2
  39. package/dist/src/commands.js.map +1 -1
  40. package/dist/src/config.js +32 -2
  41. package/dist/src/config.js.map +1 -1
  42. package/dist/src/daemon.js +50 -8
  43. package/dist/src/daemon.js.map +1 -1
  44. package/dist/src/debuground.js +1 -0
  45. package/dist/src/debuground.js.map +1 -1
  46. package/dist/src/debugroundpi.js +6 -1
  47. package/dist/src/debugroundpi.js.map +1 -1
  48. package/dist/src/dispatch.js +5 -4
  49. package/dist/src/dispatch.js.map +1 -1
  50. package/dist/src/engine/caserunner-sdk.js +15 -251
  51. package/dist/src/engine/caserunner-sdk.js.map +1 -1
  52. package/dist/src/engine/debugpi.js +17 -5
  53. package/dist/src/engine/debugpi.js.map +1 -1
  54. package/dist/src/execboundary.js +10 -3
  55. package/dist/src/execboundary.js.map +1 -1
  56. package/dist/src/github.js +51 -16
  57. package/dist/src/github.js.map +1 -1
  58. package/dist/src/githubtoken.js +346 -0
  59. package/dist/src/githubtoken.js.map +1 -0
  60. package/dist/src/home/adapters.js +349 -0
  61. package/dist/src/home/adapters.js.map +1 -0
  62. package/dist/src/home/advanced.js +318 -0
  63. package/dist/src/home/advanced.js.map +1 -0
  64. package/dist/src/home/advancedtools.js +57 -0
  65. package/dist/src/home/advancedtools.js.map +1 -0
  66. package/dist/src/home/autoreview.js +325 -0
  67. package/dist/src/home/autoreview.js.map +1 -0
  68. package/dist/src/home/daemonstatus.js +32 -0
  69. package/dist/src/home/daemonstatus.js.map +1 -0
  70. package/dist/src/home/devicelogin.js +61 -0
  71. package/dist/src/home/devicelogin.js.map +1 -0
  72. package/dist/src/home/launch.js +202 -0
  73. package/dist/src/home/launch.js.map +1 -0
  74. package/dist/src/home/loginowner.js +205 -0
  75. package/dist/src/home/loginowner.js.map +1 -0
  76. package/dist/src/home/memory.js +122 -0
  77. package/dist/src/home/memory.js.map +1 -0
  78. package/dist/src/home/model.js +811 -0
  79. package/dist/src/home/model.js.map +1 -0
  80. package/dist/src/home/panel.js +520 -0
  81. package/dist/src/home/panel.js.map +1 -0
  82. package/dist/src/home/ports.js +19 -0
  83. package/dist/src/home/ports.js.map +1 -0
  84. package/dist/src/home/review.js +163 -0
  85. package/dist/src/home/review.js.map +1 -0
  86. package/dist/src/home/reviews.js +478 -0
  87. package/dist/src/home/reviews.js.map +1 -0
  88. package/dist/src/home/setup.js +507 -0
  89. package/dist/src/home/setup.js.map +1 -0
  90. package/dist/src/home/setupadapters.js +214 -0
  91. package/dist/src/home/setupadapters.js.map +1 -0
  92. package/dist/src/home/setupcard.js +199 -0
  93. package/dist/src/home/setupcard.js.map +1 -0
  94. package/dist/src/home/setuptools.js +45 -0
  95. package/dist/src/home/setuptools.js.map +1 -0
  96. package/dist/src/home/spend.js +160 -0
  97. package/dist/src/home/spend.js.map +1 -0
  98. package/dist/src/home/switches.js +209 -0
  99. package/dist/src/home/switches.js.map +1 -0
  100. package/dist/src/home/switchon.js +439 -0
  101. package/dist/src/home/switchon.js.map +1 -0
  102. package/dist/src/home/toolnames.js +41 -0
  103. package/dist/src/home/toolnames.js.map +1 -0
  104. package/dist/src/home/tune.js +341 -0
  105. package/dist/src/home/tune.js.map +1 -0
  106. package/dist/src/home/words.js +7 -0
  107. package/dist/src/home/words.js.map +1 -0
  108. package/dist/src/homes.js +14 -0
  109. package/dist/src/homes.js.map +1 -1
  110. package/dist/src/install.js +1 -1
  111. package/dist/src/install.js.map +1 -1
  112. package/dist/src/ipc/workerspec.js +8 -0
  113. package/dist/src/ipc/workerspec.js.map +1 -1
  114. package/dist/src/live/publish.js +1 -1
  115. package/dist/src/live/publish.js.map +1 -1
  116. package/dist/src/livecli.js +1 -1
  117. package/dist/src/livecli.js.map +1 -1
  118. package/dist/src/loginhint.js +8 -3
  119. package/dist/src/loginhint.js.map +1 -1
  120. package/dist/src/loginrefresh/handover.js +121 -0
  121. package/dist/src/loginrefresh/handover.js.map +1 -0
  122. package/dist/src/loginrefresh/lock.js +54 -3
  123. package/dist/src/loginrefresh/lock.js.map +1 -1
  124. package/dist/src/loginrefresh/pirefresh.js +3 -0
  125. package/dist/src/loginrefresh/pirefresh.js.map +1 -1
  126. package/dist/src/loginrefresh/refresher.js +33 -5
  127. package/dist/src/loginrefresh/refresher.js.map +1 -1
  128. package/dist/src/loginrefresh/supervisor.js +18 -1
  129. package/dist/src/loginrefresh/supervisor.js.map +1 -1
  130. package/dist/src/npmverbs.js +46 -14
  131. package/dist/src/npmverbs.js.map +1 -1
  132. package/dist/src/onboard/choices.js +35 -4
  133. package/dist/src/onboard/choices.js.map +1 -1
  134. package/dist/src/onboard/engine.js +32 -306
  135. package/dist/src/onboard/engine.js.map +1 -1
  136. package/dist/src/onboard/pipin.js +22 -0
  137. package/dist/src/onboard/pipin.js.map +1 -0
  138. package/dist/src/onboard/stepforward.js +41 -0
  139. package/dist/src/onboard/stepforward.js.map +1 -0
  140. package/dist/src/onboard/steps/appslug.js +4 -1
  141. package/dist/src/onboard/steps/appslug.js.map +1 -1
  142. package/dist/src/onboard/steps/bringup.js +3 -2
  143. package/dist/src/onboard/steps/bringup.js.map +1 -1
  144. package/dist/src/onboard/steps/compose.js +29 -0
  145. package/dist/src/onboard/steps/compose.js.map +1 -1
  146. package/dist/src/onboard/steps/configfill.js +120 -39
  147. package/dist/src/onboard/steps/configfill.js.map +1 -1
  148. package/dist/src/onboard/steps/githubaccount.js +595 -0
  149. package/dist/src/onboard/steps/githubaccount.js.map +1 -0
  150. package/dist/src/onboard/steps/githubapp.js +65 -7
  151. package/dist/src/onboard/steps/githubapp.js.map +1 -1
  152. package/dist/src/onboard/steps/golive.js +2 -0
  153. package/dist/src/onboard/steps/golive.js.map +1 -1
  154. package/dist/src/onboard/steps/index.js +11 -0
  155. package/dist/src/onboard/steps/index.js.map +1 -1
  156. package/dist/src/onboard/steps/ingress.js +80 -25
  157. package/dist/src/onboard/steps/ingress.js.map +1 -1
  158. package/dist/src/onboard/steps/llmauth.js +103 -30
  159. package/dist/src/onboard/steps/llmauth.js.map +1 -1
  160. package/dist/src/onboard/steps/loginshare.js +213 -0
  161. package/dist/src/onboard/steps/loginshare.js.map +1 -0
  162. package/dist/src/onboard/steps/memory.js +67 -3
  163. package/dist/src/onboard/steps/memory.js.map +1 -1
  164. package/dist/src/onboard/steps/proof.js +366 -56
  165. package/dist/src/onboard/steps/proof.js.map +1 -1
  166. package/dist/src/onboard/steps/repoconfig.js +72 -0
  167. package/dist/src/onboard/steps/repoconfig.js.map +1 -0
  168. package/dist/src/onboard/steps/topology.js +6 -6
  169. package/dist/src/onboard/steps/topology.js.map +1 -1
  170. package/dist/src/onboard/steps/transport.js +9 -1
  171. package/dist/src/onboard/steps/transport.js.map +1 -1
  172. package/dist/src/onboard/steps/tuiroute.js +102 -0
  173. package/dist/src/onboard/steps/tuiroute.js.map +1 -0
  174. package/dist/src/onboard/steps/verifyup.js +17 -1
  175. package/dist/src/onboard/steps/verifyup.js.map +1 -1
  176. package/dist/src/onboard/toolnames.js +30 -0
  177. package/dist/src/onboard/toolnames.js.map +1 -0
  178. package/dist/src/operatorwrite.js +23 -0
  179. package/dist/src/operatorwrite.js.map +1 -0
  180. package/dist/src/outcomes.js +4 -1
  181. package/dist/src/outcomes.js.map +1 -1
  182. package/dist/src/piagentdir.js +13 -0
  183. package/dist/src/piagentdir.js.map +1 -0
  184. package/dist/src/pilogin.js +31 -16
  185. package/dist/src/pilogin.js.map +1 -1
  186. package/dist/src/posting-journal.js +2 -1
  187. package/dist/src/posting-journal.js.map +1 -1
  188. package/dist/src/posting-outbox.js +6 -2
  189. package/dist/src/posting-outbox.js.map +1 -1
  190. package/dist/src/posting-receipt.js +14 -1
  191. package/dist/src/posting-receipt.js.map +1 -1
  192. package/dist/src/qcrbin.js +16 -11
  193. package/dist/src/qcrbin.js.map +1 -1
  194. package/dist/src/recheck.js +2 -1
  195. package/dist/src/recheck.js.map +1 -1
  196. package/dist/src/reconcile.js +22 -2
  197. package/dist/src/reconcile.js.map +1 -1
  198. package/dist/src/reviewargs.js +122 -0
  199. package/dist/src/reviewargs.js.map +1 -0
  200. package/dist/src/reviewcli.js +7 -161
  201. package/dist/src/reviewcli.js.map +1 -1
  202. package/dist/src/reviewdetach.js +76 -11
  203. package/dist/src/reviewdetach.js.map +1 -1
  204. package/dist/src/reviewhistory.js +32 -6
  205. package/dist/src/reviewhistory.js.map +1 -1
  206. package/dist/src/reviewnow.js +45 -0
  207. package/dist/src/reviewnow.js.map +1 -0
  208. package/dist/src/scancli.js +39 -691
  209. package/dist/src/scancli.js.map +1 -1
  210. package/dist/src/selfimprove.js +66 -0
  211. package/dist/src/selfimprove.js.map +1 -0
  212. package/dist/src/setup/adapters.js +5 -223
  213. package/dist/src/setup/adapters.js.map +1 -1
  214. package/dist/src/setup/caserunner.js +15 -249
  215. package/dist/src/setup/caserunner.js.map +1 -1
  216. package/dist/src/setup/engine.js +32 -341
  217. package/dist/src/setup/engine.js.map +1 -1
  218. package/dist/src/setup/facts.js +3 -592
  219. package/dist/src/setup/facts.js.map +1 -1
  220. package/dist/src/setup/findings.js +3 -947
  221. package/dist/src/setup/findings.js.map +1 -1
  222. package/dist/src/setup/presentation.js +3 -8
  223. package/dist/src/setup/presentation.js.map +1 -1
  224. package/dist/src/setup/scan.js +3 -724
  225. package/dist/src/setup/scan.js.map +1 -1
  226. package/dist/src/setup/scanbuild.js +9 -92
  227. package/dist/src/setup/scanbuild.js.map +1 -1
  228. package/dist/src/setup/scanlease.js +3 -250
  229. package/dist/src/setup/scanlease.js.map +1 -1
  230. package/dist/src/setup/scanreport.js +3 -288
  231. package/dist/src/setup/scanreport.js.map +1 -1
  232. package/dist/src/setup/scanrun.js +3 -1732
  233. package/dist/src/setup/scanrun.js.map +1 -1
  234. package/dist/src/setup/scantree.js +3 -256
  235. package/dist/src/setup/scantree.js.map +1 -1
  236. package/dist/src/setup/stage2request.js +3 -258
  237. package/dist/src/setup/stage2request.js.map +1 -1
  238. package/dist/src/setup/unitcut.js +3 -436
  239. package/dist/src/setup/unitcut.js.map +1 -1
  240. package/dist/src/state.js +2 -0
  241. package/dist/src/state.js.map +1 -1
  242. package/dist/src/statemachine.js +4 -1
  243. package/dist/src/statemachine.js.map +1 -1
  244. package/dist/src/takeover.js +1 -0
  245. package/dist/src/takeover.js.map +1 -1
  246. package/dist/src/takeoverpi.js +15 -1
  247. package/dist/src/takeoverpi.js.map +1 -1
  248. package/dist/src/tui/app.js +49 -0
  249. package/dist/src/tui/app.js.map +1 -1
  250. package/dist/src/tui/board.js +88 -12
  251. package/dist/src/tui/board.js.map +1 -1
  252. package/dist/src/tui/cmdbar.js +4 -1
  253. package/dist/src/tui/cmdbar.js.map +1 -1
  254. package/dist/src/tui/companion.js +76 -11
  255. package/dist/src/tui/companion.js.map +1 -1
  256. package/dist/src/tui/connect.js +7 -1
  257. package/dist/src/tui/connect.js.map +1 -1
  258. package/dist/src/tui/deploytab.js +2 -2
  259. package/dist/src/tui/deploytab.js.map +1 -1
  260. package/dist/src/tui/filters.js +5 -2
  261. package/dist/src/tui/filters.js.map +1 -1
  262. package/dist/src/tui/ghread.js +61 -5
  263. package/dist/src/tui/ghread.js.map +1 -1
  264. package/dist/src/tui/index.js +109 -20
  265. package/dist/src/tui/index.js.map +1 -1
  266. package/dist/src/tui/memforward.js +39 -2
  267. package/dist/src/tui/memforward.js.map +1 -1
  268. package/dist/src/tui/memory.js +3 -16
  269. package/dist/src/tui/memory.js.map +1 -1
  270. package/dist/src/tui/memoryurl.js +22 -0
  271. package/dist/src/tui/memoryurl.js.map +1 -0
  272. package/dist/src/tui/model.js +25 -0
  273. package/dist/src/tui/model.js.map +1 -1
  274. package/dist/src/tui/prpause.js +7 -0
  275. package/dist/src/tui/prpause.js.map +1 -1
  276. package/dist/src/tui/remote.js +7 -1
  277. package/dist/src/tui/remote.js.map +1 -1
  278. package/dist/src/tui/route.js +143 -0
  279. package/dist/src/tui/route.js.map +1 -0
  280. package/dist/src/tui/setupstate.js +32 -0
  281. package/dist/src/tui/setupstate.js.map +1 -0
  282. package/dist/src/tui/sshtriage.js +102 -5
  283. package/dist/src/tui/sshtriage.js.map +1 -1
  284. package/dist/src/tui/theme.js +2 -1
  285. package/dist/src/tui/theme.js.map +1 -1
  286. package/dist/src/tui/unwedge.js +2 -12
  287. package/dist/src/tui/unwedge.js.map +1 -1
  288. package/dist/src/verdicts.js +17 -5
  289. package/dist/src/verdicts.js.map +1 -1
  290. package/dist/src/version.js +1 -1
  291. package/dist/src/version.js.map +1 -1
  292. package/dist/src/webhook.js +12 -1
  293. package/dist/src/webhook.js.map +1 -1
  294. package/dist/src/wire.js +34 -8
  295. package/dist/src/wire.js.map +1 -1
  296. package/dist/src/workerworld.js +11 -1
  297. package/dist/src/workerworld.js.map +1 -1
  298. package/docker/README.md +1 -1
  299. package/docker/companion/entrypoint.sh +20 -1
  300. package/docker/entrypoint.sh +57 -6
  301. package/docker-compose.yml +16 -1
  302. package/extensions/cli/home.ts +14 -0
  303. package/extensions/cli/login-route.ts +10 -0
  304. package/extensions/cli/selfimprove.ts +18 -0
  305. package/extensions/home.ts +484 -0
  306. package/extensions/login-route.ts +220 -0
  307. package/extensions/onboarding.ts +41 -11
  308. package/extensions/selfimprove.ts +65 -0
  309. package/extensions/setup-scan.ts +79 -766
  310. package/extensions/setup.ts +14 -70
  311. package/package.json +6 -4
  312. package/prompts/onboarding.md +58 -18
  313. package/schemas/review-result.v1.json +60 -0
  314. package/scripts/deploy.sh +8 -6
  315. package/scripts/mint-token.sh +25 -4
  316. package/scripts/qcr-host.sh +28 -5
  317. package/scripts/vm-ghcr-login.sh +7 -4
  318. package/src/archive/restore.ts +18 -21
  319. package/src/botworker.ts +13 -2
  320. package/src/channelfs.ts +3 -26
  321. package/src/ciretry.ts +2 -1
  322. package/src/cli.ts +59 -67
  323. package/src/cliops.ts +3 -2
  324. package/src/commands.ts +28 -2
  325. package/src/config.ts +62 -4
  326. package/src/daemon.ts +58 -7
  327. package/src/debuground.ts +3 -0
  328. package/src/debugroundpi.ts +12 -1
  329. package/src/dispatch.ts +5 -4
  330. package/src/engine/caserunner-sdk.ts +29 -314
  331. package/src/engine/debugpi.ts +17 -5
  332. package/src/execboundary.ts +16 -4
  333. package/src/github.ts +57 -15
  334. package/src/githubtoken.ts +435 -0
  335. package/src/home/adapters.ts +418 -0
  336. package/src/home/advanced.ts +421 -0
  337. package/src/home/advancedtools.ts +96 -0
  338. package/src/home/autoreview.ts +385 -0
  339. package/src/home/daemonstatus.ts +55 -0
  340. package/src/home/devicelogin.ts +107 -0
  341. package/src/home/launch.ts +257 -0
  342. package/src/home/loginowner.ts +248 -0
  343. package/src/home/memory.ts +176 -0
  344. package/src/home/model.ts +965 -0
  345. package/src/home/panel.ts +666 -0
  346. package/src/home/ports.ts +338 -0
  347. package/src/home/review.ts +206 -0
  348. package/src/home/reviews.ts +565 -0
  349. package/src/home/setup.ts +689 -0
  350. package/src/home/setupadapters.ts +217 -0
  351. package/src/home/setupcard.ts +288 -0
  352. package/src/home/setuptools.ts +57 -0
  353. package/src/home/spend.ts +194 -0
  354. package/src/home/switches.ts +293 -0
  355. package/src/home/switchon.ts +554 -0
  356. package/src/home/toolnames.ts +46 -0
  357. package/src/home/tune.ts +405 -0
  358. package/src/home/words.ts +8 -0
  359. package/src/homes.ts +17 -0
  360. package/src/install.ts +1 -1
  361. package/src/ipc/workerspec.ts +15 -0
  362. package/src/live/publish.ts +4 -1
  363. package/src/livecli.ts +6 -1
  364. package/src/loginhint.ts +8 -3
  365. package/src/loginrefresh/handover.ts +138 -0
  366. package/src/loginrefresh/lock.ts +72 -3
  367. package/src/loginrefresh/pirefresh.ts +3 -0
  368. package/src/loginrefresh/refresher.ts +34 -5
  369. package/src/loginrefresh/supervisor.ts +25 -2
  370. package/src/npmverbs.ts +55 -13
  371. package/src/onboard/choices.ts +40 -4
  372. package/src/onboard/engine.ts +70 -421
  373. package/src/onboard/pipin.ts +31 -0
  374. package/src/onboard/stepforward.ts +61 -0
  375. package/src/onboard/steps/appslug.ts +4 -1
  376. package/src/onboard/steps/bringup.ts +3 -2
  377. package/src/onboard/steps/compose.ts +31 -0
  378. package/src/onboard/steps/configfill.ts +144 -40
  379. package/src/onboard/steps/githubaccount.ts +716 -0
  380. package/src/onboard/steps/githubapp.ts +92 -8
  381. package/src/onboard/steps/golive.ts +2 -0
  382. package/src/onboard/steps/index.ts +11 -0
  383. package/src/onboard/steps/ingress.ts +98 -31
  384. package/src/onboard/steps/llmauth.ts +114 -29
  385. package/src/onboard/steps/loginshare.ts +258 -0
  386. package/src/onboard/steps/memory.ts +76 -3
  387. package/src/onboard/steps/proof.ts +473 -55
  388. package/src/onboard/steps/repoconfig.ts +85 -0
  389. package/src/onboard/steps/topology.ts +7 -6
  390. package/src/onboard/steps/transport.ts +15 -0
  391. package/src/onboard/steps/tuiroute.ts +119 -0
  392. package/src/onboard/steps/verifyup.ts +20 -1
  393. package/src/onboard/toolnames.ts +34 -0
  394. package/src/operatorwrite.ts +25 -0
  395. package/src/outcomes.ts +8 -1
  396. package/src/piagentdir.ts +12 -0
  397. package/src/pilogin.ts +32 -16
  398. package/src/posting-journal.ts +6 -1
  399. package/src/posting-outbox.ts +10 -2
  400. package/src/posting-receipt.ts +17 -0
  401. package/src/qcrbin.ts +16 -11
  402. package/src/recheck.ts +2 -1
  403. package/src/reconcile.ts +29 -3
  404. package/src/reviewargs.ts +119 -0
  405. package/src/reviewcli.ts +6 -167
  406. package/src/reviewdetach.ts +107 -13
  407. package/src/reviewhistory.ts +20 -6
  408. package/src/reviewnow.ts +76 -0
  409. package/src/scancli.ts +75 -826
  410. package/src/selfimprove.ts +90 -0
  411. package/src/setup/adapters.ts +13 -221
  412. package/src/setup/caserunner.ts +34 -273
  413. package/src/setup/engine.ts +62 -496
  414. package/src/setup/facts.ts +3 -627
  415. package/src/setup/findings.ts +3 -1264
  416. package/src/setup/presentation.ts +3 -18
  417. package/src/setup/scan.ts +3 -1033
  418. package/src/setup/scanbuild.ts +20 -114
  419. package/src/setup/scanlease.ts +3 -297
  420. package/src/setup/scanreport.ts +3 -373
  421. package/src/setup/scanrun.ts +3 -2268
  422. package/src/setup/scantree.ts +3 -259
  423. package/src/setup/stage2request.ts +3 -347
  424. package/src/setup/unitcut.ts +3 -523
  425. package/src/state.ts +43 -0
  426. package/src/statemachine.ts +4 -1
  427. package/src/takeover.ts +5 -0
  428. package/src/takeoverpi.ts +16 -2
  429. package/src/tui/app.ts +47 -0
  430. package/src/tui/board.ts +111 -14
  431. package/src/tui/cmdbar.ts +4 -1
  432. package/src/tui/companion.ts +80 -11
  433. package/src/tui/connect.ts +7 -1
  434. package/src/tui/deploytab.ts +2 -2
  435. package/src/tui/filters.ts +7 -3
  436. package/src/tui/ghread.ts +74 -7
  437. package/src/tui/index.ts +117 -23
  438. package/src/tui/memforward.ts +52 -3
  439. package/src/tui/memory.ts +3 -15
  440. package/src/tui/memoryurl.ts +22 -0
  441. package/src/tui/model.ts +36 -2
  442. package/src/tui/prpause.ts +11 -2
  443. package/src/tui/remote.ts +9 -1
  444. package/src/tui/route.ts +173 -0
  445. package/src/tui/setupstate.ts +56 -0
  446. package/src/tui/sshtriage.ts +127 -6
  447. package/src/tui/theme.ts +2 -1
  448. package/src/tui/unwedge.ts +2 -14
  449. package/src/verdicts.ts +23 -5
  450. package/src/version.ts +1 -1
  451. package/src/webhook.ts +10 -1
  452. package/src/wire.ts +42 -9
  453. package/src/workerworld.ts +16 -2
  454. package/dist/src/live/projection.js +0 -450
  455. package/dist/src/live/projection.js.map +0 -1
  456. package/dist/src/live/read.js +0 -60
  457. package/dist/src/live/read.js.map +0 -1
  458. package/dist/src/onboard/launch.js +0 -83
  459. package/dist/src/onboard/launch.js.map +0 -1
  460. package/dist/src/setup/ledger.js +0 -207
  461. package/dist/src/setup/ledger.js.map +0 -1
  462. package/dist/src/setup/stage2.js +0 -617
  463. package/dist/src/setup/stage2.js.map +0 -1
  464. package/dist/src/setup/textsniff.js +0 -89
  465. package/dist/src/setup/textsniff.js.map +0 -1
  466. package/src/live/projection.ts +0 -491
  467. package/src/live/read.ts +0 -86
  468. package/src/onboard/launch.ts +0 -115
  469. package/src/setup/ledger.ts +0 -324
  470. package/src/setup/stage2.ts +0 -926
  471. package/src/setup/textsniff.ts +0 -95
@@ -130,19 +130,56 @@ export function hostAuthJsonPath(home: string): string {
130
130
  * without its slash, and the entry pi writes into auth.json on /login. */
131
131
  const OAUTH_PROVIDER = OAUTH_PREFIX.replace(/\/$/, "");
132
132
 
133
- /** Does this host's auth.json hold a ChatGPT (openai-codex) OAuth login? The
134
- * file existing proves nothing: pi keeps every provider's login there, and a
135
- * logged-out pi can leave `{}` behind. Unreadable or malformed ⇒ false. */
136
- export function hostHasOAuthLogin(authPath: string): boolean {
133
+ /** pi's /login labels for the two ChatGPT subscription logins (pi 1.4.0).
134
+ * VERIFY-ON-PIN. */
135
+ export const OAUTH_LOGIN_LABEL = "OpenAI Codex (legacy)";
136
+ export const CHATGPT_LOGIN_LABEL = "Sign in with ChatGPT";
137
+
138
+ /** pi 1.4's newer "Sign in with ChatGPT" on the `openai` provider, which an
139
+ * OpenAI API key stored by /login also lands on. Every model spec reaches pi
140
+ * through the credential-aware route (packages/pi-plugin/src/modelroute.ts),
141
+ * so an `openai-codex/` model runs on this login when it is the one there:
142
+ * any OpenAI login serves the review. VERIFY-ON-PIN. */
143
+ const CHATGPT_PROVIDER = API_PREFIX.replace(/\/$/, "");
144
+
145
+ /** Where to sign in, and which logins count: what every "log in" message
146
+ * says, in pi 1.4.0's /login labels. */
147
+ export const ANY_OPENAI_LOGIN =
148
+ `any OpenAI login works: at pi's /login pick "Sign in with an account", then "OpenAI" (${CHATGPT_LOGIN_LABEL}) ` +
149
+ `or "${OAUTH_LOGIN_LABEL}"; or "Sign in with an API key", then "OpenAI"`;
150
+
151
+ /** The OpenAI logins in a pi auth.json: each ChatGPT subscription login (the
152
+ * shipped `openai-codex` and pi 1.4's newer `openai`), and whether an OpenAI
153
+ * API key was stored by /login (`{"openai":{"type":"api_key","key":…}}`).
154
+ * The file existing proves nothing: pi keeps every provider's login there,
155
+ * and a logged-out pi can leave `{}` behind. Unreadable ⇒ none. */
156
+ export function hostOpenAILogins(authPath: string): { oauth: string[]; apiKey: boolean } {
137
157
  try {
138
158
  const parsed = JSON.parse(readFileSync(authPath, "utf8")) as Record<string, unknown>;
139
- const cred = parsed?.[OAUTH_PROVIDER] as Record<string, unknown> | undefined;
140
- return typeof cred === "object" && cred !== null && cred.type === "oauth";
159
+ const cred = (provider: string): Record<string, unknown> | null => {
160
+ const c = parsed?.[provider];
161
+ return typeof c === "object" && c !== null ? (c as Record<string, unknown>) : null;
162
+ };
163
+ const key = cred(CHATGPT_PROVIDER);
164
+ return {
165
+ oauth: [OAUTH_PROVIDER, CHATGPT_PROVIDER].filter((p) => cred(p)?.type === "oauth"),
166
+ apiKey: key?.type === "api_key" && typeof key.key === "string" && key.key.trim() !== "",
167
+ };
141
168
  } catch {
142
- return false;
169
+ return { oauth: [], apiKey: false };
143
170
  }
144
171
  }
145
172
 
173
+ /** Does this host's auth.json hold a ChatGPT subscription login (either one)? */
174
+ export function hostHasOAuthLogin(authPath: string): boolean {
175
+ return hostOAuthProviders(authPath).length > 0;
176
+ }
177
+
178
+ /** Every ChatGPT subscription login in this auth.json. Unreadable ⇒ none. */
179
+ export function hostOAuthProviders(authPath: string): string[] {
180
+ return hostOpenAILogins(authPath).oauth;
181
+ }
182
+
146
183
  function hostHome(): string {
147
184
  return process.env.HOME ?? homedir();
148
185
  }
@@ -356,18 +393,24 @@ export type LoginState =
356
393
  "absent" | "no-pi-volume" | "stopped" | "no-auth" | "wrong-provider" | "ok" | "undetectable";
357
394
 
358
395
  /**
359
- * The in-container login check: the same question `hostHasOAuthLogin` asks of
360
- * the host — is there an `openai-codex` entry of type `oauth`? — asked by the
361
- * container's own node, so only an exit code ever leaves it (never a token,
362
- * which on the VM route would otherwise cross ssh). Exit 0 = logged in,
363
- * 3 = no auth.json / empty, 4 = an auth.json without that login (`{}`, or
364
- * another provider picked at pi's /login menu). Anything else (no node) is
365
- * undetectable, never a pass.
396
+ * The in-container login check: the same question `hostOpenAILogins` asks of
397
+ * the host — is there an OpenAI login? — asked by the container's own node, so
398
+ * only an exit code ever leaves it (never a token, which on the VM route would
399
+ * otherwise cross ssh). Any OpenAI login counts, because the engine runs the
400
+ * same model on whichever exists (packages/pi-plugin/src/modelroute.ts): an
401
+ * `openai-codex` or `openai` entry of type `oauth`, or an `openai` API key.
402
+ * Exit 0 = logged in, 3 = no auth.json / empty, 4 = an auth.json without an
403
+ * OpenAI login (`{}`, or another provider picked at pi's /login menu).
404
+ * Anything else (no node) is undetectable, never a pass.
366
405
  */
367
406
  export const CONTAINER_LOGIN_SCRIPT =
368
407
  "const fs=require('fs');let t='';try{t=fs.readFileSync(process.argv[1],'utf8')}catch{process.exit(3)}" +
369
408
  "if(!t.trim())process.exit(3);let a;try{a=JSON.parse(t)}catch{process.exit(4)}" +
370
- `const c=a&&a[${JSON.stringify(OAUTH_PROVIDER)}];process.exit(c&&typeof c==='object'&&c.type==='oauth'?0:4)`;
409
+ "const c=(k)=>{const v=a&&a[k];return !!v&&typeof v==='object'?v:null};" +
410
+ "const o=(k)=>c(k)?.type==='oauth';" +
411
+ `const key=c(${JSON.stringify(CHATGPT_PROVIDER)});` +
412
+ "const k=!!key&&key.type==='api_key'&&typeof key.key==='string'&&key.key.trim()!=='';" +
413
+ `process.exit(o(${JSON.stringify(OAUTH_PROVIDER)})||o(${JSON.stringify(CHATGPT_PROVIDER)})||k?0:4)`;
371
414
 
372
415
  export interface LoginProbe {
373
416
  state: LoginState;
@@ -483,8 +526,8 @@ function loginCheck(
483
526
  name,
484
527
  ok: false,
485
528
  detail:
486
- `${label}: auth.json on the pi home (${probe.mount}) holds no ChatGPT (${OAUTH_PROVIDER}) OAuth login — ` +
487
- "a /login to another provider does not count. Attach and log in with the ChatGPT option: " +
529
+ `${label}: auth.json on the pi home (${probe.mount}) holds no OpenAI login — ` +
530
+ `a /login to another provider does not count. Attach and log in (${ANY_OPENAI_LOGIN}): ` +
488
531
  `${steps}.`,
489
532
  };
490
533
  case "no-pi-volume":
@@ -575,7 +618,7 @@ export function companionSessionName(configText: string): string {
575
618
 
576
619
  // ---- the operator-facing text ----------------------------------------------
577
620
 
578
- export const ROUTE_QUESTION =
621
+ const ROUTE_CHOICES =
579
622
  "How should the review sessions authenticate to OpenAI?\n" +
580
623
  " 1) oauth — a ChatGPT subscription. Cost is your plan, flat. The catch: pi's OAuth tokens live in the\n" +
581
624
  " pi home of whichever process logged in and DO NOT transfer between this host and the containers, so\n" +
@@ -583,8 +626,29 @@ export const ROUTE_QUESTION =
583
626
  " companion container. Each is guided when its surface first needs it.\n" +
584
627
  " 2) api — one OpenAI API key, metered per token. One paste covers the daemon and its review sessions (the daemon\n" +
585
628
  " container's entrypoint exports the key file as OPENAI_API_KEY), and the wizard rewrites all five model strings\n" +
586
- " for you. The companion container is not handed the key: it keeps its own login.\n" +
587
- "Answer 1/oauth or 2/api:";
629
+ " for you. The companion container is not handed the key: it keeps its own login.\n";
630
+
631
+ export const ROUTE_QUESTION = `${ROUTE_CHOICES}Answer 1/oauth or 2/api:`;
632
+
633
+ /** The same question where no prompter can ask it: the pi-driven wizard
634
+ * passes the operator's answer as this step's `route` input. */
635
+ export const ROUTE_INPUT_QUESTION =
636
+ `${ROUTE_CHOICES}Ask the operator which one, then run this step again with route: "oauth" or route: "api". ` +
637
+ "Never ask them to type the answer anywhere else: there is no other prompt.";
638
+
639
+ /** The step's input schema: the operator's route answer (pi-driven flow). */
640
+ const ROUTE_PARAMETERS = {
641
+ type: "object",
642
+ properties: {
643
+ route: {
644
+ type: "string",
645
+ enum: ["oauth", "api"],
646
+ description:
647
+ 'The operator\'s answer: "oauth" (a ChatGPT subscription) or "api" (an OpenAI API key). ' +
648
+ "Omit it to keep the recorded route, or to read both options out.",
649
+ },
650
+ },
651
+ } as const;
588
652
 
589
653
  export const KEY_QUESTION = `paste the OpenAI API key (it is written straight to the ${OPENAI_KEY_FILE} secrets file, 0600, never to a command line):`;
590
654
 
@@ -635,12 +699,16 @@ interface RouteDecision {
635
699
  * route is what pi will really use, so the config must say so;
636
700
  * 3. otherwise a valid key file;
637
701
  * 4. otherwise the previously recorded route;
638
- * 5. otherwise: no answer yet — the step goes red with the choice text.
702
+ * 5. otherwise an OpenAI API key this host's pi stored at /login: the
703
+ * wizard's own session runs on a key, so the api route is the one in use
704
+ * (the daemon still takes its key from the secrets file);
705
+ * 6. otherwise: no answer yet — the step goes red with the choice text.
639
706
  */
640
707
  async function decideRoute(
641
708
  ctx: StepContext,
642
709
  keyFile: KeyFileState,
643
710
  hostLogin: boolean,
711
+ hostApiKey = false,
644
712
  ): Promise<RouteDecision> {
645
713
  const prompter = ctx.adapters.prompter;
646
714
  if (prompter) {
@@ -652,15 +720,24 @@ async function decideRoute(
652
720
  return { route: kept, source: "kept the recorded route (empty answer)" };
653
721
  return { route: null, source: `did not understand the answer — reply 1/oauth or 2/api` };
654
722
  }
723
+ const answered = typeof ctx.input.route === "string" ? parseRouteAnswer(ctx.input.route) : null;
724
+ if (answered) return { route: answered, source: "you chose it" };
655
725
  if (hostLogin)
656
726
  return {
657
727
  route: "oauth",
658
- source: "a completed pi login is on this host, and pi prefers auth.json over any env key",
728
+ source:
729
+ "a completed ChatGPT login is on this host, and pi prefers auth.json over any env key",
659
730
  };
660
731
  if (keyFile.valid)
661
732
  return { route: "api", source: `a valid ${OPENAI_KEY_FILE} is in the secrets dir` };
662
733
  const kept = recordedRoute(ctx);
663
734
  if (kept) return { route: kept, source: "the recorded route" };
735
+ if (hostApiKey)
736
+ return {
737
+ route: "api",
738
+ source:
739
+ "this host's pi signed in with an OpenAI API key (its /login), and holds no ChatGPT login",
740
+ };
664
741
  return { route: null, source: "nothing on disk says which route this installation uses" };
665
742
  }
666
743
 
@@ -793,20 +870,22 @@ async function oauthLoginChecks(
793
870
  ): Promise<CheckResult[]> {
794
871
  const checks: CheckResult[] = [];
795
872
  const authPath = hostAuthJsonPath(home);
796
- const hostOk = hostHasOAuthLogin(authPath);
873
+ const logins = hostOpenAILogins(authPath);
874
+ const hostOk = logins.oauth.length > 0 || logins.apiKey;
797
875
  checks.push(
798
876
  hostOk
799
877
  ? {
800
878
  name: "wizard_login",
801
879
  ok: true,
802
- detail: `this host's pi is logged in (${authPath}) — login 1 of 3 done`,
880
+ detail: `this host's pi is logged in (${authPath}, ${[...logins.oauth, ...(logins.apiKey ? ["an OpenAI API key"] : [])].join(" + ")}) — login 1 of 3 done`,
803
881
  }
804
882
  : {
805
883
  name: "wizard_login",
806
884
  ok: false,
807
885
  detail:
808
- `no ChatGPT (${OAUTH_PROVIDER}) OAuth login in this host's pi (${authPath}). Log in from this very session — type /login at pi's input line ` +
809
- "and follow the browser flow — then run this step again. This is login 1 of 3; the two container logins come later.",
886
+ `no OpenAI login in this host's pi (${authPath}). Log in from this very session — type /login at pi's input line ` +
887
+ `(${ANY_OPENAI_LOGIN}) and follow its flow — then run this step again. This is login 1 of 3; ` +
888
+ "the two container logins come later.",
810
889
  },
811
890
  );
812
891
  const runner = ctx.adapters.runner;
@@ -852,6 +931,7 @@ export function llmAuthStep(opts: { transport?: TransportOptions } = {}): Onboar
852
931
  // After preflight (10) and the topology question (20): the spec's second
853
932
  // question, and everything after it assumes a decided route.
854
933
  order: 30,
934
+ parameters: ROUTE_PARAMETERS,
855
935
 
856
936
  async run(ctx): Promise<StepReport> {
857
937
  const home = hostHome();
@@ -878,14 +958,19 @@ export function llmAuthStep(opts: { transport?: TransportOptions } = {}): Onboar
878
958
  rootOf(ctx.adapters),
879
959
  );
880
960
  let keyFile = readKeyFileState(secretsDir);
881
- const hostLogin = hostHasOAuthLogin(hostAuthJsonPath(home));
961
+ const logins = hostOpenAILogins(hostAuthJsonPath(home));
962
+ const hostLogin = logins.oauth.length > 0;
882
963
 
883
- const decision = await decideRoute(ctx, keyFile, hostLogin);
964
+ const decision = await decideRoute(ctx, keyFile, hostLogin, logins.apiKey);
884
965
  if (decision.route === null) {
885
- checks.push({ name: "route", ok: false, detail: `${decision.source}.\n${ROUTE_QUESTION}` });
966
+ const question = ctx.adapters.prompter ? ROUTE_QUESTION : ROUTE_INPUT_QUESTION;
967
+ checks.push({ name: "route", ok: false, detail: `${decision.source}.\n${question}` });
886
968
  return { ok: false, checks };
887
969
  }
888
970
  const route = decision.route;
971
+ // An answer is recorded at once, so a re-run (a login still owed, say)
972
+ // keeps it without asking again.
973
+ if (decision.source === "you chose it") ctx.recordChoice(CHOICE_KEYS.authRoute, route);
889
974
  checks.push({ name: "route", ok: true, detail: `${route} route — ${decision.source}` });
890
975
 
891
976
  let credentialChanged =
@@ -0,0 +1,258 @@
1
+ // Switch-on's login step: one OpenAI login per box. The daemon's
2
+ // container reviews on the login the home signed in to, the host pi home's
3
+ // auth.json, instead of a login of its own.
4
+ //
5
+ // .env QCR_PI_LOGIN_DIR = the home's pi agent dir. docker-compose.yml
6
+ // mounts it at /opt/qcr/login and names it to the daemon
7
+ // (QCR_LOGIN_DIR), so the refresher keeps that one file and every
8
+ // worker borrows it; pi's settings stay in the container's own
9
+ // pi home, never the person's.
10
+ // config [login_refresh] enabled = true, so the daemon runs its single
11
+ // refresher on that file while auto-review runs. The config must
12
+ // still load with it (its margin outlasts the longest worker
13
+ // cap), or nothing is written.
14
+ // restart a stack brought up before is recreated, so the daemon has the
15
+ // mount and reads the config at boot (its volumes, so its data,
16
+ // are kept). A first switch-on's bring-up starts it this way.
17
+ // proof a stack brought up before keeps its image pin and its extracted
18
+ // docker-compose.yml, which may be an older release's. So the
19
+ // compose file must define the mount before anything is written,
20
+ // and the running daemon must show it reviews on the login (its
21
+ // QCR_LOGIN_DIR, the login in the mount, an image whose
22
+ // refresher reads that dir) before the step says so. Until then
23
+ // the step is not done, and the next switch-on runs it again.
24
+ //
25
+ // The hand-over needs nothing more: while the refresher runs it refreshes far
26
+ // ahead of pi's own window, so the home's pi, a borrower now, never has a
27
+ // token to refresh; switching auto-review off stops the daemon, the refresher
28
+ // with it, and the home's pi refreshes on its own again (src/home/loginowner.ts).
29
+ // The single-instance lock lives beside the shared auth.json, so a second
30
+ // refresher on this box is refused whoever starts it.
31
+ //
32
+ // A remote server (the advanced route) never gets this step: it signs in on
33
+ // its own device login, and the host's login is never copied there.
34
+ import { existsSync, readFileSync } from "node:fs";
35
+ import { join } from "node:path";
36
+ import { parse } from "smol-toml";
37
+ import { parseConfig } from "../../config.js";
38
+ import type {
39
+ CheckResult,
40
+ OnboardingStep,
41
+ ProcessRunner,
42
+ StepContext,
43
+ StepReport,
44
+ } from "../engine.js";
45
+ import { CONTAINER_LOGIN_DIR, LOGIN_DIR_ENV } from "../../loginrefresh/supervisor.js";
46
+ import { BROUGHT_UP_CHOICE_KEY } from "./bringup.js";
47
+ import {
48
+ DAEMON_SERVICE,
49
+ composeArgs,
50
+ daemonExec,
51
+ readEnvText,
52
+ releaseVersion,
53
+ rootOf,
54
+ writeAtomic,
55
+ } from "./compose.js";
56
+ import { assertNoDryRunEdit, patchToml, type TomlEdit } from "./configfill.js";
57
+ import { readEnvKey, setEnvKey } from "./ingress.js";
58
+ import { topologyOf } from "./topology.js";
59
+
60
+ export const LOGIN_SHARE_STEP_ID = "login_share";
61
+ /** The project .env key docker-compose.yml mounts the host's login from. */
62
+ export const PI_LOGIN_DIR_ENV = "QCR_PI_LOGIN_DIR";
63
+
64
+ export interface LoginShareOptions {
65
+ /** The home's pi agent dir, where its login is. Absent: nothing is shared
66
+ * and the daemon keeps a login of its own. */
67
+ loginDir?: string;
68
+ }
69
+
70
+ const firstLine = (s: string): string => s.trim().split("\n")[0]?.trim() || "no output";
71
+ const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
72
+
73
+ /** Does the config run the refresher? null when it cannot be read. */
74
+ export function refresherEnabled(configText: string): boolean | null {
75
+ try {
76
+ const t = (parse(configText) as Record<string, unknown>)["login_refresh"];
77
+ return (
78
+ typeof t === "object" && t !== null && (t as Record<string, unknown>)["enabled"] === true
79
+ );
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /** The config with the refresher on; null when it is on already. */
86
+ export function refresherPatch(configText: string): string | null {
87
+ if (refresherEnabled(configText) === true) return null;
88
+ const edits: TomlEdit[] = [{ section: "login_refresh", key: "enabled", value: "true" }];
89
+ assertNoDryRunEdit(edits);
90
+ return patchToml(configText, edits);
91
+ }
92
+
93
+ const fail = (name: string, detail: string): StepReport => ({
94
+ ok: false,
95
+ checks: [{ name, ok: false, detail }],
96
+ });
97
+
98
+ /** The upgrade a stack too old for a shared login needs, said once. */
99
+ function upgradeFix(root: string): string {
100
+ const version = releaseVersion(root);
101
+ return (
102
+ `Fix: update the stack in ${root} to a release with the shared login` +
103
+ `${version ? ` (QCR_VERSION=${version} in its .env, and that release's docker-compose.yml)` : ""}, ` +
104
+ "then switch auto-review on again"
105
+ );
106
+ }
107
+
108
+ /** Does the project's docker-compose.yml mount a shared login? null when
109
+ * there is no compose file yet (the bring-up places it, this release's). */
110
+ export function composeMountsLogin(root: string): boolean | null {
111
+ let text: string;
112
+ try {
113
+ text = readFileSync(join(root, "docker-compose.yml"), "utf8");
114
+ } catch {
115
+ return null;
116
+ }
117
+ return text.includes(PI_LOGIN_DIR_ENV);
118
+ }
119
+
120
+ /** The running daemon, asked whether it reviews on the shared login: the
121
+ * compose definition hands it the dir, the mount holds the login, and its
122
+ * image's refresher reads that dir. Exit codes name which failed. */
123
+ export const DAEMON_LOGIN_PROBE = [
124
+ "sh",
125
+ "-c",
126
+ `test "$${LOGIN_DIR_ENV}" = ${CONTAINER_LOGIN_DIR} || exit 3; ` +
127
+ `test -s ${CONTAINER_LOGIN_DIR}/auth.json || exit 4; ` +
128
+ `grep -q ${LOGIN_DIR_ENV} dist/src/loginrefresh/supervisor.js 2>/dev/null || exit 5`,
129
+ ] as const;
130
+
131
+ /** Ask the running daemon (DAEMON_LOGIN_PROBE). A daemon that cannot be
132
+ * asked is not one that reviews on the login: never claimed. */
133
+ export async function daemonLoginCheck(runner: ProcessRunner, root: string): Promise<CheckResult> {
134
+ const r = await daemonExec(runner, root, DAEMON_LOGIN_PROBE);
135
+ if (r.code === 0)
136
+ return { name: "daemon", ok: true, detail: "the daemon reviews on your login, data kept" };
137
+ const why =
138
+ r.code === 3
139
+ ? `the running daemon is not handed the login (its ${LOGIN_DIR_ENV} is not ${CONTAINER_LOGIN_DIR}): the stack's docker-compose.yml is an older release's`
140
+ : r.code === 4
141
+ ? `the running daemon sees no login at ${CONTAINER_LOGIN_DIR}: the stack's docker-compose.yml does not mount it`
142
+ : r.code === 5
143
+ ? "the running daemon's image predates the shared login: it would keep reviewing on a login of its own"
144
+ : `can't ask the running daemon whether it reviews on your login: ${firstLine(r.stderr || r.stdout)}`;
145
+ return { name: "daemon", ok: false, detail: `${why}. ${upgradeFix(root)}` };
146
+ }
147
+
148
+ export function loginShareStep(o: LoginShareOptions = {}): OnboardingStep {
149
+ const state = async (ctx: StepContext): Promise<StepReport> => {
150
+ if (o.loginDir === undefined)
151
+ return {
152
+ ok: true,
153
+ checks: [{ name: "login", ok: true, detail: "no home login to share here" }],
154
+ };
155
+ const env = readEnvKey(readEnvText(rootOf(ctx.adapters)), PI_LOGIN_DIR_ENV);
156
+ let config: string;
157
+ try {
158
+ config = readFileSync(ctx.adapters.paths.configPath, "utf8");
159
+ } catch (err) {
160
+ return fail("login", `can't read ${ctx.adapters.paths.configPath}: ${message(err)}`);
161
+ }
162
+ const checks: CheckResult[] = [
163
+ env === o.loginDir
164
+ ? { name: "login", ok: true, detail: `auto-review reviews on your login in ${o.loginDir}` }
165
+ : {
166
+ name: "login",
167
+ ok: false,
168
+ detail: `the stack does not mount your login (${PI_LOGIN_DIR_ENV} is ${env === null ? "unset" : JSON.stringify(env)})`,
169
+ },
170
+ refresherEnabled(config) === true
171
+ ? { name: "refresher", ok: true, detail: "its refresher keeps the login fresh" }
172
+ : { name: "refresher", ok: false, detail: "[login_refresh] is not enabled" },
173
+ ];
174
+ return { ok: checks.every((c) => c.ok), checks };
175
+ };
176
+
177
+ return {
178
+ id: LOGIN_SHARE_STEP_ID,
179
+ title: "Share your login with auto-review",
180
+ explain:
181
+ "Auto-review reviews on the OpenAI login this home signed in to: one login per machine, which its refresher keeps fresh while it runs.",
182
+ // after the config steps (memory is 60), before the daemon starts (69)
183
+ order: 66,
184
+ enabled: (choices) => topologyOf(choices) === "local",
185
+ async run(ctx): Promise<StepReport> {
186
+ const dir = o.loginDir;
187
+ if (dir === undefined) return state(ctx);
188
+ const auth = join(dir, "auth.json");
189
+ if (!existsSync(auth))
190
+ return fail(
191
+ "login",
192
+ `there is no login at ${auth} yet: sign in in the home first, then switch auto-review on again`,
193
+ );
194
+ // compose expands `$` in an .env value and treats ` #` as a comment
195
+ if (/[$\n]|\s#/.test(dir))
196
+ return fail(
197
+ "login",
198
+ `${dir} can't be passed to Docker Compose through .env (a \`$\`, a newline or a space before #): set PI_CODING_AGENT_DIR to a plainer path`,
199
+ );
200
+ const configPath = ctx.adapters.paths.configPath;
201
+ let config: string;
202
+ try {
203
+ config = readFileSync(configPath, "utf8");
204
+ } catch (err) {
205
+ return fail("refresher", `can't read ${configPath}: ${message(err)}`);
206
+ }
207
+ const patched = refresherPatch(config);
208
+ if (patched !== null) {
209
+ try {
210
+ parseConfig(patched);
211
+ } catch (err) {
212
+ return fail(
213
+ "refresher",
214
+ `[login_refresh] enabled = true would not load in ${configPath}: ${message(err)} — nothing was written`,
215
+ );
216
+ }
217
+ }
218
+ const root = rootOf(ctx.adapters);
219
+ if (composeMountsLogin(root) === false)
220
+ return fail(
221
+ "daemon",
222
+ `${join(root, "docker-compose.yml")} is an older release's: it does not mount a shared login, so the daemon would keep a login of its own. Nothing was written. ${upgradeFix(root)}`,
223
+ );
224
+ const envText = readEnvText(root);
225
+ const nextEnv = setEnvKey(envText, PI_LOGIN_DIR_ENV, dir);
226
+ if (patched !== null) writeAtomic(configPath, patched);
227
+ if (nextEnv !== envText) writeAtomic(join(root, ".env"), nextEnv);
228
+ const checks: CheckResult[] = [
229
+ {
230
+ name: "login",
231
+ ok: true,
232
+ detail: `auto-review reviews on your login in ${dir}, and its refresher keeps it fresh while it runs`,
233
+ },
234
+ ];
235
+ const changed = patched !== null || nextEnv !== envText;
236
+ if (typeof ctx.choices[BROUGHT_UP_CHOICE_KEY] === "string") {
237
+ if (changed) {
238
+ // the mount and the config are read when the container starts
239
+ const r = await ctx.adapters.runner.run(
240
+ "docker",
241
+ composeArgs(root, ["up", "-d", "--no-deps", "--force-recreate", DAEMON_SERVICE]),
242
+ );
243
+ if (r.code !== 0)
244
+ return fail(
245
+ "daemon",
246
+ `\`docker compose up -d --force-recreate ${DAEMON_SERVICE}\` exited ${r.code}: ${firstLine(r.stderr || r.stdout)}`,
247
+ );
248
+ }
249
+ // a stack brought up before says so itself, or the step is not done
250
+ const daemon = await daemonLoginCheck(ctx.adapters.runner, root);
251
+ checks.push(daemon);
252
+ if (!daemon.ok) return { ok: false, checks };
253
+ }
254
+ return { ok: true, checks };
255
+ },
256
+ verify: state,
257
+ };
258
+ }
@@ -77,7 +77,7 @@ import { dirname, join, resolve, sep } from "node:path";
77
77
  import { pathToFileURL } from "node:url";
78
78
  import { OPENVIKING_DEFAULTS, parseConfig } from "../../config.js";
79
79
  import { configHome, locateKeysFile, locateSecrets } from "../../install.js";
80
- import { tuiMemoryBaseUrl } from "../../tui/memory.js";
80
+ import { tuiMemoryBaseUrl } from "../../tui/memoryurl.js";
81
81
  import { shellQuote } from "../../tui/remote.js";
82
82
  import { CHOICE_KEYS, MEMORY_CHOICE_IDS, type MemoryChoice } from "../choices.js";
83
83
  import { deliverConfigChange } from "./configdelivery.js";
@@ -481,6 +481,22 @@ export const DECLINED_CHECKS: readonly CheckResult[] = [
481
481
  /** The check row that reports the daemon's on/off switch. */
482
482
  export const MEMORY_SWITCH_CHECK = "memory-switch";
483
483
 
484
+ /** The check row that says phase B waits on a server not answering yet. */
485
+ export const MEMORY_SERVER_CHECK = "memory-server";
486
+
487
+ /** A run that landed phase A (the root key) and stopped only because the
488
+ * server is not up yet: on a stack not running memory, the bring-up starts
489
+ * it and the memory_user_keys step finishes phase B. Any other failure is a
490
+ * failure. */
491
+ export function waitsOnServer(checks: readonly CheckResult[]): boolean {
492
+ const failed = checks.filter((c) => !c.ok);
493
+ return (
494
+ failed.length > 0 &&
495
+ failed.every((c) => c.name === MEMORY_SERVER_CHECK) &&
496
+ checks.some((c) => c.name === "root-key" && c.ok)
497
+ );
498
+ }
499
+
484
500
  /**
485
501
  * Tell the DAEMON the operator's answer: `[openviking] enabled =
486
502
  * false` on a decline — its word for "off", under which memory health reports
@@ -700,6 +716,15 @@ function serverUnreachableDetail(ctx: StepContext, base: string, why: string): s
700
716
  const flags = forwardFlags(ctx);
701
717
  const cmd = (sub: string): string =>
702
718
  `\`cd ${root} && node scripts/companion-memory-forward.mjs ${sub} ${flags}\``;
719
+ if (topologyOf(ctx.choices) !== "vm")
720
+ return (
721
+ `no OpenViking answering at ${base} (${why}). The root key is in place, which is what the daemon needs at boot. ` +
722
+ "Before the bring-up that is expected, and nothing is needed from you: the bring-up step starts the memory engine, " +
723
+ "and the memory_user_keys step after it reaches it through a memory forward the wizard opens itself, then mints " +
724
+ "the two user keys. If the stack is already up, the memory container is the thing to check " +
725
+ "(`docker compose ps memory`, `docker compose logs memory`); a forward that stays up outside the wizard is " +
726
+ `${cmd("install")} (the systemd unit) or \`qcr tui\`, which starts one.`
727
+ );
703
728
  return (
704
729
  `no OpenViking answering at ${base} (${why}). The root key is in place, which is what the daemon needs at boot. ` +
705
730
  "The memory engine listens only on its own container's loopback (the memory sidecar on the laptop route), so this machine reaches it through the memory " +
@@ -896,7 +921,7 @@ export function memoryStep(opts: MemoryStepOptions = {}): OnboardingStep {
896
921
  };
897
922
  };
898
923
 
899
- return {
924
+ const step: OnboardingStep = {
900
925
  id: MEMORY_STEP_ID,
901
926
  title: "Memory: mint, place and confine the OpenViking keys",
902
927
  explain:
@@ -1000,7 +1025,7 @@ export function memoryStep(opts: MemoryStepOptions = {}): OnboardingStep {
1000
1025
  const ready = await serverReady(ctx.adapters.fetch, base, prov);
1001
1026
  if (!ready.ok) {
1002
1027
  checks.push({
1003
- name: "memory-server",
1028
+ name: MEMORY_SERVER_CHECK,
1004
1029
  ok: false,
1005
1030
  detail: ready.answering
1006
1031
  ? `OpenViking answers at ${base} but is not ready (${ready.detail}). Check the engine and embedder logs, then run this step again; the memory forward already reaches it.`
@@ -1216,6 +1241,54 @@ export function memoryStep(opts: MemoryStepOptions = {}): OnboardingStep {
1216
1241
  return { ok: checks.every((c) => c.ok), checks };
1217
1242
  },
1218
1243
  };
1244
+
1245
+ // On the compose route the engine is reachable from this machine only
1246
+ // through the memory forward, and the wizard runs before any TUI has opened
1247
+ // one: each call holds one open for its own duration (adopting a running
1248
+ // forward, closing one it started). A declined memory needs no server.
1249
+ const withForward = async (
1250
+ ctx: StepContext,
1251
+ body: () => Promise<StepReport>,
1252
+ ): Promise<StepReport> => {
1253
+ const open = ctx.adapters.memoryForward;
1254
+ const declined =
1255
+ ctx.input.choice === "declined" ||
1256
+ (ctx.input.choice === undefined && ctx.choices[MEMORY_CHOICE_KEY] === "declined");
1257
+ if (!open || declined || topologyOf(ctx.choices) === "vm") return body();
1258
+ const facts = remoteFactsFor(ctx.adapters, ctx.choices);
1259
+ let forward: Awaited<ReturnType<typeof open>> = null;
1260
+ try {
1261
+ forward = await open({
1262
+ baseUrl: resolvePaths(opts, ctx).base,
1263
+ repoRoot: rootOf(ctx.adapters),
1264
+ container: facts.memoryContainer,
1265
+ sudoDocker: facts.sudo,
1266
+ log: (line) => ctx.adapters.log?.(line),
1267
+ });
1268
+ } catch {
1269
+ forward = null;
1270
+ }
1271
+ try {
1272
+ const report = await body();
1273
+ return forward?.started
1274
+ ? {
1275
+ ...report,
1276
+ checks: [
1277
+ { name: "memory-forward", ok: true, detail: forward.detail },
1278
+ ...report.checks,
1279
+ ],
1280
+ }
1281
+ : report;
1282
+ } finally {
1283
+ forward?.stop();
1284
+ }
1285
+ };
1286
+
1287
+ return {
1288
+ ...step,
1289
+ run: (ctx) => withForward(ctx, () => step.run(ctx)),
1290
+ verify: (ctx) => withForward(ctx, () => step.verify(ctx)),
1291
+ };
1219
1292
  }
1220
1293
 
1221
1294
  /**