@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 +1 -1
- package/src/core/connection.ts +18 -1
- package/src/core/env.ts +8 -0
- package/src/core/serve-manager.ts +10 -1
- package/src/index.tsx +19 -0
- package/src/render/ink/App.tsx +18 -1
- package/src/render/ink/screens/ErrorScreen.tsx +17 -1
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.
|
|
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",
|
package/src/core/connection.ts
CHANGED
|
@@ -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 =
|
|
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:
|
|
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
|
|
package/src/render/ink/App.tsx
CHANGED
|
@@ -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?",
|