@quickchatai/quickreview 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/README.md +35 -19
  2. package/config.example.toml +41 -9
  3. package/dist/extensions/cli/login-route.js +5 -0
  4. package/dist/extensions/cli/login-route.js.map +1 -0
  5. package/dist/extensions/login-route.js +180 -0
  6. package/dist/extensions/login-route.js.map +1 -0
  7. package/dist/extensions/onboarding.js +29 -6
  8. package/dist/extensions/onboarding.js.map +1 -1
  9. package/dist/src/ciretry.js +2 -1
  10. package/dist/src/ciretry.js.map +1 -1
  11. package/dist/src/cli.js +43 -12
  12. package/dist/src/cli.js.map +1 -1
  13. package/dist/src/cliops.js +3 -2
  14. package/dist/src/cliops.js.map +1 -1
  15. package/dist/src/commands.js +25 -2
  16. package/dist/src/commands.js.map +1 -1
  17. package/dist/src/config.js +16 -2
  18. package/dist/src/config.js.map +1 -1
  19. package/dist/src/daemon.js +24 -8
  20. package/dist/src/daemon.js.map +1 -1
  21. package/dist/src/debugroundpi.js +5 -1
  22. package/dist/src/debugroundpi.js.map +1 -1
  23. package/dist/src/dispatch.js +5 -4
  24. package/dist/src/dispatch.js.map +1 -1
  25. package/dist/src/engine/debugpi.js +17 -5
  26. package/dist/src/engine/debugpi.js.map +1 -1
  27. package/dist/src/execboundary.js +10 -3
  28. package/dist/src/execboundary.js.map +1 -1
  29. package/dist/src/github.js +51 -16
  30. package/dist/src/github.js.map +1 -1
  31. package/dist/src/githubtoken.js +346 -0
  32. package/dist/src/githubtoken.js.map +1 -0
  33. package/dist/src/loginhint.js +8 -3
  34. package/dist/src/loginhint.js.map +1 -1
  35. package/dist/src/loginrefresh/pirefresh.js +3 -0
  36. package/dist/src/loginrefresh/pirefresh.js.map +1 -1
  37. package/dist/src/npmverbs.js +4 -2
  38. package/dist/src/npmverbs.js.map +1 -1
  39. package/dist/src/onboard/choices.js +29 -1
  40. package/dist/src/onboard/choices.js.map +1 -1
  41. package/dist/src/onboard/engine.js +8 -23
  42. package/dist/src/onboard/engine.js.map +1 -1
  43. package/dist/src/onboard/launch.js +12 -20
  44. package/dist/src/onboard/launch.js.map +1 -1
  45. package/dist/src/onboard/pipin.js +22 -0
  46. package/dist/src/onboard/pipin.js.map +1 -0
  47. package/dist/src/onboard/stepforward.js +41 -0
  48. package/dist/src/onboard/stepforward.js.map +1 -0
  49. package/dist/src/onboard/steps/appslug.js +4 -1
  50. package/dist/src/onboard/steps/appslug.js.map +1 -1
  51. package/dist/src/onboard/steps/compose.js +29 -0
  52. package/dist/src/onboard/steps/compose.js.map +1 -1
  53. package/dist/src/onboard/steps/configfill.js +102 -32
  54. package/dist/src/onboard/steps/configfill.js.map +1 -1
  55. package/dist/src/onboard/steps/githubaccount.js +595 -0
  56. package/dist/src/onboard/steps/githubaccount.js.map +1 -0
  57. package/dist/src/onboard/steps/githubapp.js +65 -7
  58. package/dist/src/onboard/steps/githubapp.js.map +1 -1
  59. package/dist/src/onboard/steps/golive.js +2 -0
  60. package/dist/src/onboard/steps/golive.js.map +1 -1
  61. package/dist/src/onboard/steps/index.js +11 -0
  62. package/dist/src/onboard/steps/index.js.map +1 -1
  63. package/dist/src/onboard/steps/ingress.js +80 -25
  64. package/dist/src/onboard/steps/ingress.js.map +1 -1
  65. package/dist/src/onboard/steps/llmauth.js +103 -30
  66. package/dist/src/onboard/steps/llmauth.js.map +1 -1
  67. package/dist/src/onboard/steps/memory.js +53 -1
  68. package/dist/src/onboard/steps/memory.js.map +1 -1
  69. package/dist/src/onboard/steps/proof.js +366 -56
  70. package/dist/src/onboard/steps/proof.js.map +1 -1
  71. package/dist/src/onboard/steps/topology.js +6 -6
  72. package/dist/src/onboard/steps/topology.js.map +1 -1
  73. package/dist/src/onboard/steps/transport.js +9 -1
  74. package/dist/src/onboard/steps/transport.js.map +1 -1
  75. package/dist/src/onboard/steps/tuiroute.js +102 -0
  76. package/dist/src/onboard/steps/tuiroute.js.map +1 -0
  77. package/dist/src/onboard/steps/verifyup.js +17 -1
  78. package/dist/src/onboard/steps/verifyup.js.map +1 -1
  79. package/dist/src/onboard/toolnames.js +30 -0
  80. package/dist/src/onboard/toolnames.js.map +1 -0
  81. package/dist/src/operatorwrite.js +23 -0
  82. package/dist/src/operatorwrite.js.map +1 -0
  83. package/dist/src/outcomes.js +4 -1
  84. package/dist/src/outcomes.js.map +1 -1
  85. package/dist/src/posting-journal.js +2 -1
  86. package/dist/src/posting-journal.js.map +1 -1
  87. package/dist/src/posting-outbox.js +6 -2
  88. package/dist/src/posting-outbox.js.map +1 -1
  89. package/dist/src/posting-receipt.js +14 -1
  90. package/dist/src/posting-receipt.js.map +1 -1
  91. package/dist/src/recheck.js +2 -1
  92. package/dist/src/recheck.js.map +1 -1
  93. package/dist/src/reconcile.js +22 -2
  94. package/dist/src/reconcile.js.map +1 -1
  95. package/dist/src/reviewhistory.js +32 -6
  96. package/dist/src/reviewhistory.js.map +1 -1
  97. package/dist/src/reviewnow.js +45 -0
  98. package/dist/src/reviewnow.js.map +1 -0
  99. package/dist/src/state.js.map +1 -1
  100. package/dist/src/statemachine.js +4 -1
  101. package/dist/src/statemachine.js.map +1 -1
  102. package/dist/src/takeoverpi.js +13 -0
  103. package/dist/src/takeoverpi.js.map +1 -1
  104. package/dist/src/tui/app.js +49 -0
  105. package/dist/src/tui/app.js.map +1 -1
  106. package/dist/src/tui/board.js +88 -12
  107. package/dist/src/tui/board.js.map +1 -1
  108. package/dist/src/tui/cmdbar.js +4 -1
  109. package/dist/src/tui/cmdbar.js.map +1 -1
  110. package/dist/src/tui/companion.js +74 -10
  111. package/dist/src/tui/companion.js.map +1 -1
  112. package/dist/src/tui/connect.js +7 -1
  113. package/dist/src/tui/connect.js.map +1 -1
  114. package/dist/src/tui/filters.js +5 -2
  115. package/dist/src/tui/filters.js.map +1 -1
  116. package/dist/src/tui/ghread.js +61 -5
  117. package/dist/src/tui/ghread.js.map +1 -1
  118. package/dist/src/tui/index.js +106 -20
  119. package/dist/src/tui/index.js.map +1 -1
  120. package/dist/src/tui/memforward.js +39 -2
  121. package/dist/src/tui/memforward.js.map +1 -1
  122. package/dist/src/tui/model.js +25 -0
  123. package/dist/src/tui/model.js.map +1 -1
  124. package/dist/src/tui/prpause.js +7 -0
  125. package/dist/src/tui/prpause.js.map +1 -1
  126. package/dist/src/tui/remote.js +7 -1
  127. package/dist/src/tui/remote.js.map +1 -1
  128. package/dist/src/tui/route.js +143 -0
  129. package/dist/src/tui/route.js.map +1 -0
  130. package/dist/src/tui/setupstate.js +29 -0
  131. package/dist/src/tui/setupstate.js.map +1 -0
  132. package/dist/src/tui/sshtriage.js +102 -5
  133. package/dist/src/tui/sshtriage.js.map +1 -1
  134. package/dist/src/tui/theme.js +2 -1
  135. package/dist/src/tui/theme.js.map +1 -1
  136. package/dist/src/tui/unwedge.js +2 -12
  137. package/dist/src/tui/unwedge.js.map +1 -1
  138. package/dist/src/verdicts.js +17 -5
  139. package/dist/src/verdicts.js.map +1 -1
  140. package/dist/src/version.js +1 -1
  141. package/dist/src/version.js.map +1 -1
  142. package/dist/src/webhook.js +12 -1
  143. package/dist/src/webhook.js.map +1 -1
  144. package/dist/src/wire.js +28 -6
  145. package/dist/src/wire.js.map +1 -1
  146. package/dist/src/workerworld.js +7 -1
  147. package/dist/src/workerworld.js.map +1 -1
  148. package/docker/companion/entrypoint.sh +20 -1
  149. package/docker/entrypoint.sh +17 -0
  150. package/extensions/cli/login-route.ts +10 -0
  151. package/extensions/login-route.ts +220 -0
  152. package/extensions/onboarding.ts +35 -6
  153. package/package.json +2 -2
  154. package/prompts/onboarding.md +57 -18
  155. package/scripts/mint-token.sh +25 -4
  156. package/src/ciretry.ts +2 -1
  157. package/src/cli.ts +49 -14
  158. package/src/cliops.ts +3 -2
  159. package/src/commands.ts +28 -2
  160. package/src/config.ts +24 -4
  161. package/src/daemon.ts +32 -7
  162. package/src/debugroundpi.ts +11 -1
  163. package/src/dispatch.ts +5 -4
  164. package/src/engine/debugpi.ts +17 -5
  165. package/src/execboundary.ts +16 -4
  166. package/src/github.ts +57 -15
  167. package/src/githubtoken.ts +435 -0
  168. package/src/loginhint.ts +8 -3
  169. package/src/loginrefresh/pirefresh.ts +3 -0
  170. package/src/npmverbs.ts +4 -2
  171. package/src/onboard/choices.ts +34 -1
  172. package/src/onboard/engine.ts +22 -3
  173. package/src/onboard/launch.ts +17 -29
  174. package/src/onboard/pipin.ts +31 -0
  175. package/src/onboard/stepforward.ts +61 -0
  176. package/src/onboard/steps/appslug.ts +4 -1
  177. package/src/onboard/steps/compose.ts +31 -0
  178. package/src/onboard/steps/configfill.ts +121 -33
  179. package/src/onboard/steps/githubaccount.ts +716 -0
  180. package/src/onboard/steps/githubapp.ts +92 -8
  181. package/src/onboard/steps/golive.ts +2 -0
  182. package/src/onboard/steps/index.ts +11 -0
  183. package/src/onboard/steps/ingress.ts +98 -31
  184. package/src/onboard/steps/llmauth.ts +114 -29
  185. package/src/onboard/steps/memory.ts +58 -1
  186. package/src/onboard/steps/proof.ts +473 -55
  187. package/src/onboard/steps/topology.ts +7 -6
  188. package/src/onboard/steps/transport.ts +15 -0
  189. package/src/onboard/steps/tuiroute.ts +119 -0
  190. package/src/onboard/steps/verifyup.ts +20 -1
  191. package/src/onboard/toolnames.ts +34 -0
  192. package/src/operatorwrite.ts +25 -0
  193. package/src/outcomes.ts +8 -1
  194. package/src/posting-journal.ts +6 -1
  195. package/src/posting-outbox.ts +10 -2
  196. package/src/posting-receipt.ts +17 -0
  197. package/src/recheck.ts +2 -1
  198. package/src/reconcile.ts +29 -3
  199. package/src/reviewhistory.ts +20 -6
  200. package/src/reviewnow.ts +76 -0
  201. package/src/state.ts +18 -0
  202. package/src/statemachine.ts +4 -1
  203. package/src/takeoverpi.ts +13 -0
  204. package/src/tui/app.ts +47 -0
  205. package/src/tui/board.ts +111 -14
  206. package/src/tui/cmdbar.ts +4 -1
  207. package/src/tui/companion.ts +78 -10
  208. package/src/tui/connect.ts +7 -1
  209. package/src/tui/filters.ts +7 -3
  210. package/src/tui/ghread.ts +74 -7
  211. package/src/tui/index.ts +114 -23
  212. package/src/tui/memforward.ts +52 -3
  213. package/src/tui/model.ts +36 -2
  214. package/src/tui/prpause.ts +11 -2
  215. package/src/tui/remote.ts +9 -1
  216. package/src/tui/route.ts +173 -0
  217. package/src/tui/setupstate.ts +52 -0
  218. package/src/tui/sshtriage.ts +127 -6
  219. package/src/tui/theme.ts +2 -1
  220. package/src/tui/unwedge.ts +2 -14
  221. package/src/verdicts.ts +23 -5
  222. package/src/version.ts +1 -1
  223. package/src/webhook.ts +10 -1
  224. package/src/wire.ts +36 -7
  225. package/src/workerworld.ts +7 -1
@@ -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 =
@@ -700,6 +700,15 @@ function serverUnreachableDetail(ctx: StepContext, base: string, why: string): s
700
700
  const flags = forwardFlags(ctx);
701
701
  const cmd = (sub: string): string =>
702
702
  `\`cd ${root} && node scripts/companion-memory-forward.mjs ${sub} ${flags}\``;
703
+ if (topologyOf(ctx.choices) !== "vm")
704
+ return (
705
+ `no OpenViking answering at ${base} (${why}). The root key is in place, which is what the daemon needs at boot. ` +
706
+ "Before the bring-up that is expected, and nothing is needed from you: the bring-up step starts the memory engine, " +
707
+ "and the memory_user_keys step after it reaches it through a memory forward the wizard opens itself, then mints " +
708
+ "the two user keys. If the stack is already up, the memory container is the thing to check " +
709
+ "(`docker compose ps memory`, `docker compose logs memory`); a forward that stays up outside the wizard is " +
710
+ `${cmd("install")} (the systemd unit) or \`qcr tui\`, which starts one.`
711
+ );
703
712
  return (
704
713
  `no OpenViking answering at ${base} (${why}). The root key is in place, which is what the daemon needs at boot. ` +
705
714
  "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 +905,7 @@ export function memoryStep(opts: MemoryStepOptions = {}): OnboardingStep {
896
905
  };
897
906
  };
898
907
 
899
- return {
908
+ const step: OnboardingStep = {
900
909
  id: MEMORY_STEP_ID,
901
910
  title: "Memory: mint, place and confine the OpenViking keys",
902
911
  explain:
@@ -1216,6 +1225,54 @@ export function memoryStep(opts: MemoryStepOptions = {}): OnboardingStep {
1216
1225
  return { ok: checks.every((c) => c.ok), checks };
1217
1226
  },
1218
1227
  };
1228
+
1229
+ // On the compose route the engine is reachable from this machine only
1230
+ // through the memory forward, and the wizard runs before any TUI has opened
1231
+ // one: each call holds one open for its own duration (adopting a running
1232
+ // forward, closing one it started). A declined memory needs no server.
1233
+ const withForward = async (
1234
+ ctx: StepContext,
1235
+ body: () => Promise<StepReport>,
1236
+ ): Promise<StepReport> => {
1237
+ const open = ctx.adapters.memoryForward;
1238
+ const declined =
1239
+ ctx.input.choice === "declined" ||
1240
+ (ctx.input.choice === undefined && ctx.choices[MEMORY_CHOICE_KEY] === "declined");
1241
+ if (!open || declined || topologyOf(ctx.choices) === "vm") return body();
1242
+ const facts = remoteFactsFor(ctx.adapters, ctx.choices);
1243
+ let forward: Awaited<ReturnType<typeof open>> = null;
1244
+ try {
1245
+ forward = await open({
1246
+ baseUrl: resolvePaths(opts, ctx).base,
1247
+ repoRoot: rootOf(ctx.adapters),
1248
+ container: facts.memoryContainer,
1249
+ sudoDocker: facts.sudo,
1250
+ log: (line) => ctx.adapters.log?.(line),
1251
+ });
1252
+ } catch {
1253
+ forward = null;
1254
+ }
1255
+ try {
1256
+ const report = await body();
1257
+ return forward?.started
1258
+ ? {
1259
+ ...report,
1260
+ checks: [
1261
+ { name: "memory-forward", ok: true, detail: forward.detail },
1262
+ ...report.checks,
1263
+ ],
1264
+ }
1265
+ : report;
1266
+ } finally {
1267
+ forward?.stop();
1268
+ }
1269
+ };
1270
+
1271
+ return {
1272
+ ...step,
1273
+ run: (ctx) => withForward(ctx, () => step.run(ctx)),
1274
+ verify: (ctx) => withForward(ctx, () => step.verify(ctx)),
1275
+ };
1219
1276
  }
1220
1277
 
1221
1278
  /**