@kidsinai/kids-client 0.0.24 → 0.0.26
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/models.ts +25 -0
- package/src/core/serve-manager.ts +13 -0
- package/src/index.tsx +54 -2
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.26",
|
|
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/models.ts
CHANGED
|
@@ -69,3 +69,28 @@ function pickModels(models: unknown): unknown[] {
|
|
|
69
69
|
if (models && typeof models === "object") return Object.values(models)
|
|
70
70
|
return []
|
|
71
71
|
}
|
|
72
|
+
|
|
73
|
+
/** True when an LLM error is about the *model itself* (not auth/network) — e.g.
|
|
74
|
+
* a model the current ChatGPT-account auth isn't allowed to use. The fix is to
|
|
75
|
+
* switch models, so callers should re-pick rather than retry or re-auth. */
|
|
76
|
+
export function isModelUnavailable(msg: string): boolean {
|
|
77
|
+
const m = msg.toLowerCase()
|
|
78
|
+
return m.includes("not supported")
|
|
79
|
+
|| m.includes("model_not_found")
|
|
80
|
+
|| m.includes("does not have access")
|
|
81
|
+
|| m.includes("not available")
|
|
82
|
+
|| m.includes("unsupported model")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Pick a kid-safe default model: prefer the small/standard tiers that work
|
|
86
|
+
* with ChatGPT-account auth; skip the `-pro` tiers (API-key only, rejected by
|
|
87
|
+
* the Codex/OAuth path). Returns null only if the server reports no models. */
|
|
88
|
+
export function pickDefaultModel(models: ModelChoice[]): ModelChoice | null {
|
|
89
|
+
const usable = models.filter((m) => !/-pro\b/i.test(m.id))
|
|
90
|
+
const prefer = ["gpt-5.4-mini", "gpt-5.4", "claude-3-5-sonnet", "sonnet", "gpt-5.5"]
|
|
91
|
+
for (const p of prefer) {
|
|
92
|
+
const hit = usable.find((m) => m.id.toLowerCase().includes(p))
|
|
93
|
+
if (hit) return hit
|
|
94
|
+
}
|
|
95
|
+
return usable[0] ?? models[0] ?? null
|
|
96
|
+
}
|
|
@@ -56,6 +56,16 @@ export interface ServeManagerOptions {
|
|
|
56
56
|
*/
|
|
57
57
|
serverUsername: string
|
|
58
58
|
opencodeBin: string
|
|
59
|
+
/**
|
|
60
|
+
* Inline opencode config (JSON string) passed to the spawned serve via
|
|
61
|
+
* OPENCODE_CONFIG_CONTENT. opencode otherwise reads the empty global config
|
|
62
|
+
* and falls back to a default model the ChatGPT-account auth can't use
|
|
63
|
+
* (gpt-5.5-pro) with permissions unset. Passing a curated `{ model,
|
|
64
|
+
* permission }` here pins a usable default model AND enforces the kid-safety
|
|
65
|
+
* "ask before write/edit" gate at the source. (OPENCODE_CONFIG as a file
|
|
66
|
+
* path is unreliable; the inline content var is what opencode honors.)
|
|
67
|
+
*/
|
|
68
|
+
opencodeConfigContent?: string
|
|
59
69
|
/** Max total wait for readiness in ms. Default {@link DEFAULT_READY_TIMEOUT_MS}. */
|
|
60
70
|
readyTimeoutMs?: number
|
|
61
71
|
/** Per-probe abort ceiling in ms. Default {@link DEFAULT_PROBE_TIMEOUT_MS}. */
|
|
@@ -101,6 +111,9 @@ export class ServeManager {
|
|
|
101
111
|
...process.env,
|
|
102
112
|
OPENCODE_SERVER_PASSWORD: this.opts.serverPassword,
|
|
103
113
|
OPENCODE_SERVER_USERNAME: this.opts.serverUsername,
|
|
114
|
+
...(this.opts.opencodeConfigContent
|
|
115
|
+
? { OPENCODE_CONFIG_CONTENT: this.opts.opencodeConfigContent }
|
|
116
|
+
: {}),
|
|
104
117
|
},
|
|
105
118
|
stdout: "pipe",
|
|
106
119
|
stderr: "pipe",
|
package/src/index.tsx
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import React from "react"
|
|
21
21
|
import { render } from "ink"
|
|
22
22
|
import { join } from "node:path"
|
|
23
|
+
import { readFileSync } from "node:fs"
|
|
23
24
|
import { readEnv, validateEnv, type KidsClientEnv } from "./core/env.ts"
|
|
24
25
|
import { ServeManager } from "./core/serve-manager.ts"
|
|
25
26
|
import { createKidsClient, type OpencodeClient } from "./core/connection.ts"
|
|
@@ -31,7 +32,7 @@ import { listInstalledPacks, resolveContext } from "./core/course-pack.ts"
|
|
|
31
32
|
import { readLastSession, writeLastSession } from "./core/last-session.ts"
|
|
32
33
|
import { isCompletionTrigger, runCheck } from "./core/check-runner.ts"
|
|
33
34
|
import { parseSlash, matchCommand } from "./core/commands.ts"
|
|
34
|
-
import { listModels } from "./core/models.ts"
|
|
35
|
+
import { listModels, isModelUnavailable, pickDefaultModel } from "./core/models.ts"
|
|
35
36
|
import { findFiles } from "./core/files.ts"
|
|
36
37
|
import { App } from "./render/ink/App.tsx"
|
|
37
38
|
import { FREE_PLAY_PACK_ID } from "./render/ink/screens/CoursePackPicker.tsx"
|
|
@@ -344,6 +345,7 @@ async function bootServices(env: KidsClientEnv, store: Store): Promise<ServiceSe
|
|
|
344
345
|
serverPassword: env.opencodeServerPassword,
|
|
345
346
|
serverUsername: env.opencodeServerUsername,
|
|
346
347
|
opencodeBin: env.opencodeBin,
|
|
348
|
+
opencodeConfigContent: buildServeConfig(env.configDir),
|
|
347
349
|
onAuditLine: (event) => {
|
|
348
350
|
audit.push(event)
|
|
349
351
|
store.pushAudit(event)
|
|
@@ -652,10 +654,32 @@ function makeFullHandlers(
|
|
|
652
654
|
|
|
653
655
|
store.update({ thinking: true })
|
|
654
656
|
updateLastSession()
|
|
657
|
+
// Resolve a usable model up front. With no explicit pick, the server
|
|
658
|
+
// falls back to whatever model it last used — which may be one the
|
|
659
|
+
// current auth can't use (a ChatGPT-account login can't use gpt-5.5-pro).
|
|
660
|
+
// Pinning a known-good default makes the kid's first message just work.
|
|
661
|
+
let model = snap.selectedModel
|
|
662
|
+
if (!model) {
|
|
663
|
+
const def = pickDefaultModel(await listModels(client))
|
|
664
|
+
if (def) {
|
|
665
|
+
model = def.id
|
|
666
|
+
store.update({ selectedModel: def.id, selectedModelLabel: def.label })
|
|
667
|
+
}
|
|
668
|
+
}
|
|
655
669
|
try {
|
|
656
|
-
await session.prompt(text, { model:
|
|
670
|
+
await session.prompt(text, { model: model ?? undefined })
|
|
657
671
|
} catch (err) {
|
|
658
672
|
const detail = errMessage(err)
|
|
673
|
+
if (isModelUnavailable(detail)) {
|
|
674
|
+
// The model isn't usable on this account (e.g. a -pro model under a
|
|
675
|
+
// ChatGPT login). Clear it so the next message auto-picks a good
|
|
676
|
+
// default, and guide the kid to /model instead of a scary error.
|
|
677
|
+
store.update({ thinking: false, selectedModel: null, selectedModelLabel: null })
|
|
678
|
+
sysMessage(env.locale === "zh-Hans"
|
|
679
|
+
? "这个 AI 模型在你的账号下用不了。直接再发一条消息会自动换成可用模型,或打 /model 自己选(推荐 gpt-5.4-mini)。"
|
|
680
|
+
: "That AI model isn't available on your account. Just send again to auto-switch to a usable one, or type /model to choose (try gpt-5.4-mini).")
|
|
681
|
+
return
|
|
682
|
+
}
|
|
659
683
|
store.update({ thinking: false, screen: { kind: "error", variant: classifyLlmError(detail), detail } })
|
|
660
684
|
}
|
|
661
685
|
},
|
|
@@ -893,6 +917,34 @@ function errMessage(err: unknown): string {
|
|
|
893
917
|
return String(err)
|
|
894
918
|
}
|
|
895
919
|
|
|
920
|
+
/**
|
|
921
|
+
* Build the inline opencode config passed to the spawned serve. opencode's
|
|
922
|
+
* own global config is effectively empty, so without this it defaults the
|
|
923
|
+
* openai provider to gpt-5.5-pro (rejected by ChatGPT-account auth) with no
|
|
924
|
+
* permission gate. We forward a curated, schema-clean subset of the kids
|
|
925
|
+
* preset: a usable default model + the kid-safety "ask before acting" gate.
|
|
926
|
+
*
|
|
927
|
+
* Deliberately NOT forwarded: the preset's `agent.tools` / `_comment` keys —
|
|
928
|
+
* they fail opencode's config schema (which silently drops the whole config).
|
|
929
|
+
* The tool whitelist is enforced by the kids plugin regardless, so nothing is
|
|
930
|
+
* lost. Returns undefined if the preset is missing/unreadable (serve then
|
|
931
|
+
* falls back to its own config — same as before this change).
|
|
932
|
+
*/
|
|
933
|
+
function buildServeConfig(configDir: string): string | undefined {
|
|
934
|
+
let model = "openai/gpt-5.4-mini"
|
|
935
|
+
try {
|
|
936
|
+
const preset = JSON.parse(readFileSync(join(configDir, "opencode.json"), "utf8")) as { model?: string }
|
|
937
|
+
if (typeof preset.model === "string" && preset.model.includes("/")) model = preset.model
|
|
938
|
+
} catch {
|
|
939
|
+
return undefined
|
|
940
|
+
}
|
|
941
|
+
return JSON.stringify({
|
|
942
|
+
$schema: "https://opencode.ai/config.json",
|
|
943
|
+
model,
|
|
944
|
+
permission: { edit: "ask", write: "ask", bash: "ask", webfetch: "ask" },
|
|
945
|
+
})
|
|
946
|
+
}
|
|
947
|
+
|
|
896
948
|
void main().catch((err) => {
|
|
897
949
|
console.error("kids-client: fatal startup error:", err)
|
|
898
950
|
process.exit(1)
|