@kidsinai/kids-client 0.0.10 → 0.0.11

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@kidsinai/kids-client",
4
- "version": "0.0.10",
4
+ "version": "0.0.11",
5
5
  "type": "module",
6
6
  "description": "Own-client TUI for Kids OpenCode — talks to local `opencode serve` via @opencode-ai/sdk v2 with kid-warm rendering, mission progress, permission dialog, and stderr-tail audit pipeline.",
7
7
  "license": "MIT",
@@ -13,10 +13,17 @@ export type OpencodeClient = ReturnType<typeof createOpencodeClient>
13
13
  export interface ConnectionOptions {
14
14
  baseUrl: string
15
15
  serverPassword: string
16
+ /**
17
+ * Must match upstream's `OPENCODE_SERVER_USERNAME` (default "opencode").
18
+ * Sending an empty username here yields a 401 against opencode ≥1.x —
19
+ * see opencode-kernel server/auth.ts `authorized()` which requires
20
+ * `credentials.username === config.username`.
21
+ */
22
+ serverUsername: string
16
23
  }
17
24
 
18
25
  export function createKidsClient(opts: ConnectionOptions): OpencodeClient {
19
- const authHeader = "Basic " + btoa(`:${opts.serverPassword}`)
26
+ const authHeader = buildAuthHeader(opts.serverUsername, opts.serverPassword)
20
27
  return createOpencodeClient({
21
28
  baseUrl: opts.baseUrl,
22
29
  headers: {
@@ -24,3 +31,13 @@ export function createKidsClient(opts: ConnectionOptions): OpencodeClient {
24
31
  },
25
32
  } as Parameters<typeof createOpencodeClient>[0])
26
33
  }
34
+
35
+ /**
36
+ * Construct the HTTP Basic-Auth header opencode serve expects.
37
+ * Exposed so serve-manager's probe + tests can share the exact format
38
+ * upstream `authorized()` checks against (username MUST match config,
39
+ * default "opencode" — empty username produces 401 on opencode ≥1.x).
40
+ */
41
+ export function buildAuthHeader(username: string, password: string): string {
42
+ return "Basic " + btoa(`${username}:${password}`)
43
+ }
package/src/core/env.ts CHANGED
@@ -14,6 +14,13 @@ export interface KidsClientEnv {
14
14
  opencodeBaseUrl: string
15
15
  /** HTTP Basic Auth password for serve. Mandatory. */
16
16
  opencodeServerPassword: string
17
+ /**
18
+ * HTTP Basic Auth *username* for serve. Defaults to "opencode" to match
19
+ * upstream's `OPENCODE_SERVER_USERNAME` default (opencode-kernel
20
+ * packages/opencode/src/server/auth.ts) — sending an empty username here
21
+ * causes a 401 against opencode ≥1.x even when the password is correct.
22
+ */
23
+ opencodeServerUsername: string
17
24
  /** DeepRouter tenant key. May be empty when using BYOK bypass. */
18
25
  deeprouterApiKey: string
19
26
  /** True if the wrapper set KIDS_LLM_BYPASS_GATEWAY=1 (BYOK dogfood mode). */
@@ -39,6 +46,7 @@ export function readEnv(): KidsClientEnv {
39
46
  return {
40
47
  opencodeBaseUrl: process.env.OPENCODE_BASE_URL ?? "http://127.0.0.1:4096",
41
48
  opencodeServerPassword: password,
49
+ opencodeServerUsername: process.env.OPENCODE_SERVER_USERNAME || "opencode",
42
50
  deeprouterApiKey: process.env.DEEPROUTER_API_KEY ?? "",
43
51
  bypassGateway: process.env.KIDS_LLM_BYPASS_GATEWAY === "1",
44
52
  coursePack: process.env.KIDS_COURSE_PACK || null,
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { spawn, type Subprocess } from "bun"
13
+ import { buildAuthHeader } from "./connection.ts"
13
14
 
14
15
  export type ServeReadiness =
15
16
  | { kind: "already_running" }
@@ -32,6 +33,13 @@ export type ProbeResult = "ok" | "auth_mismatch" | "offline"
32
33
  export interface ServeManagerOptions {
33
34
  baseUrl: string
34
35
  serverPassword: string
36
+ /**
37
+ * Must match upstream's `OPENCODE_SERVER_USERNAME` (default "opencode").
38
+ * The probe sends this as the Basic-auth username — upstream's
39
+ * authorized() requires an exact match, so an empty username produces
40
+ * a 401 even when the password is correct.
41
+ */
42
+ serverUsername: string
35
43
  opencodeBin: string
36
44
  /** Max wait for readiness probe in ms. Default 10s. */
37
45
  readyTimeoutMs?: number
@@ -75,6 +83,7 @@ export class ServeManager {
75
83
  env: {
76
84
  ...process.env,
77
85
  OPENCODE_SERVER_PASSWORD: this.opts.serverPassword,
86
+ OPENCODE_SERVER_USERNAME: this.opts.serverUsername,
78
87
  },
79
88
  stdout: "pipe",
80
89
  stderr: "pipe",
@@ -121,7 +130,7 @@ export class ServeManager {
121
130
  try {
122
131
  const res = await fetch(`${this.opts.baseUrl}/app`, {
123
132
  headers: {
124
- authorization: "Basic " + btoa(`:${this.opts.serverPassword}`),
133
+ authorization: buildAuthHeader(this.opts.serverUsername, this.opts.serverPassword),
125
134
  },
126
135
  })
127
136
  return classifyProbeStatus(res.status)
package/src/index.tsx CHANGED
@@ -64,6 +64,7 @@ interface AppHandlers {
64
64
  onPermissionReply: (decision: "allow" | "deny" | "edit") => void
65
65
  onDangerousAcknowledge: () => void
66
66
  onErrorRetry: () => void | Promise<void>
67
+ onReconfigure: () => void
67
68
  onQuit: () => void | Promise<void>
68
69
  onAbort: () => void
69
70
  onHelpBack: () => void
@@ -189,6 +190,12 @@ function makeHandlers(
189
190
  // Pre-boot error retry: re-run main isn't trivial; just exit.
190
191
  process.exit(1)
191
192
  },
193
+ onReconfigure: () => {
194
+ // From an error screen, jump into the setup wizard so the parent can
195
+ // change provider / paste a new API key. onSetupContinue knows whether
196
+ // we're in first-run (resolve gate) or post-boot (reload env + retry).
197
+ store.update({ screen: { kind: "setup" } })
198
+ },
192
199
  onQuit: async () => {
193
200
  const s = servicesHolder.current
194
201
  if (s) return s.quit()
@@ -211,6 +218,16 @@ function makeHandlers(
211
218
  onSetupContinue: async () => {
212
219
  const r = getResolveSetup()
213
220
  if (r) r()
221
+ // Post-boot reconfigure path: services are already up but the env they
222
+ // were booted with is stale. Re-source the env file (the wizard wrote
223
+ // it) and replay the same recovery as [Enter] Retry on the error
224
+ // screen — push the loading screen and re-run readiness probe.
225
+ const s = servicesHolder.current
226
+ if (s) {
227
+ reloadEnvFile(env.configDir)
228
+ Object.assign(env, readEnv())
229
+ await s.handlers.onErrorRetry()
230
+ }
214
231
  },
215
232
  onSetupSkip: () => {
216
233
  const r = getResolveSetup()
@@ -245,6 +262,7 @@ async function bootServices(env: KidsClientEnv, store: Store): Promise<ServiceSe
245
262
  const serve = new ServeManager({
246
263
  baseUrl: env.opencodeBaseUrl,
247
264
  serverPassword: env.opencodeServerPassword,
265
+ serverUsername: env.opencodeServerUsername,
248
266
  opencodeBin: env.opencodeBin,
249
267
  onAuditLine: (event) => {
250
268
  audit.push(event)
@@ -282,6 +300,7 @@ async function bootServices(env: KidsClientEnv, store: Store): Promise<ServiceSe
282
300
  const client = createKidsClient({
283
301
  baseUrl: env.opencodeBaseUrl,
284
302
  serverPassword: env.opencodeServerPassword,
303
+ serverUsername: env.opencodeServerUsername,
285
304
  })
286
305
  const session = new SessionManager(client)
287
306
 
@@ -11,7 +11,7 @@
11
11
 
12
12
  import React, { useSyncExternalStore } from "react"
13
13
  import type { InstalledPack } from "../../core/course-pack.ts"
14
- import type { Store } from "../../core/store.ts"
14
+ import type { ErrorVariant, Store } from "../../core/store.ts"
15
15
  import { StartupScreen } from "./screens/StartupScreen.tsx"
16
16
  import { MissionScreen } from "./screens/MissionScreen.tsx"
17
17
  import { PermissionModal } from "./screens/PermissionModal.tsx"
@@ -25,6 +25,17 @@ import { SetupScreen } from "./screens/SetupScreen.tsx"
25
25
  import { TourScreen } from "./screens/TourScreen.tsx"
26
26
  import type { ProviderId } from "../../core/setup.ts"
27
27
 
28
+ // Variants where the root cause may be a stale / wrong API key or a missing
29
+ // provider config — showing a [c] Change settings option that opens the setup
30
+ // wizard actually helps. Pure runtime/network problems (network_down,
31
+ // stars_exhausted, ai_hung) are excluded; they need a different recovery.
32
+ const RECONFIGURABLE_VARIANTS: ReadonlySet<ErrorVariant> = new Set([
33
+ "serve_unreachable",
34
+ "port_taken",
35
+ "auth_failed",
36
+ "config_missing",
37
+ ])
38
+
28
39
  export interface AppDeps {
29
40
  store: Store
30
41
  locale: "zh-Hans" | "en"
@@ -34,6 +45,11 @@ export interface AppDeps {
34
45
  onPermissionReply: (decision: "allow" | "deny" | "edit") => void
35
46
  onDangerousAcknowledge: () => void
36
47
  onErrorRetry: () => void
48
+ /**
49
+ * Jump from the error screen into the setup wizard. Only wired for
50
+ * config-related variants — see RECONFIGURABLE_VARIANTS below.
51
+ */
52
+ onReconfigure: () => void
37
53
  onQuit: () => void
38
54
  onAbort: () => void
39
55
  onHelpBack: () => void
@@ -117,6 +133,7 @@ export function App(deps: AppDeps): React.ReactElement {
117
133
  detail={state.screen.detail}
118
134
  locale={deps.locale}
119
135
  onRetry={deps.onErrorRetry}
136
+ onReconfigure={RECONFIGURABLE_VARIANTS.has(state.screen.variant) ? deps.onReconfigure : undefined}
120
137
  onQuit={deps.onQuit}
121
138
  />
122
139
  )
@@ -21,12 +21,20 @@ interface ErrorScreenProps {
21
21
  detail?: string
22
22
  onRetry?: () => void
23
23
  onQuit?: () => void
24
+ /**
25
+ * Open the setup wizard so the parent can change provider / paste a new
26
+ * API key. Wired by AppDeps only for config-related variants
27
+ * (serve_unreachable / port_taken / auth_failed / config_missing) — retry
28
+ * alone won't fix a wrong key.
29
+ */
30
+ onReconfigure?: () => void
24
31
  }
25
32
 
26
- export function ErrorScreen({ variant, locale, detail, onRetry, onQuit }: ErrorScreenProps): React.ReactElement {
33
+ export function ErrorScreen({ variant, locale, detail, onRetry, onQuit, onReconfigure }: ErrorScreenProps): React.ReactElement {
27
34
  const theme = getTheme()
28
35
  useInput((input, key) => {
29
36
  if (key.return && onRetry) onRetry()
37
+ else if ((input === "c" || input === "C") && onReconfigure) onReconfigure()
30
38
  else if (input === "q" && onQuit) onQuit()
31
39
  })
32
40
  const t = STRINGS[locale][variant]
@@ -52,6 +60,12 @@ export function ErrorScreen({ variant, locale, detail, onRetry, onQuit }: ErrorS
52
60
  <Text color={theme.fg}> {t.retry}</Text>
53
61
  </Box>
54
62
  )}
63
+ {onReconfigure && (
64
+ <Box marginRight={2}>
65
+ <Text color={theme.accent}>[c]</Text>
66
+ <Text color={theme.fg}> {STRINGS[locale].reconfigure}</Text>
67
+ </Box>
68
+ )}
55
69
  {onQuit && (
56
70
  <Box>
57
71
  <Text color={theme.accent}>[q]</Text>
@@ -66,6 +80,7 @@ export function ErrorScreen({ variant, locale, detail, onRetry, onQuit }: ErrorS
66
80
  const STRINGS = {
67
81
  "zh-Hans": {
68
82
  quit: "退出",
83
+ reconfigure: "改设置(换 key / 换 provider)",
69
84
  serve_unreachable: {
70
85
  title: "AI 老师还没起来",
71
86
  body: "后台 AI 服务好像没启动。要不要再试一次?",
@@ -104,6 +119,7 @@ const STRINGS = {
104
119
  },
105
120
  en: {
106
121
  quit: "Quit",
122
+ reconfigure: "Change settings (switch key / provider)",
107
123
  serve_unreachable: {
108
124
  title: "AI teacher didn't start",
109
125
  body: "The background AI service isn't running. Try again?",