@kidsinai/kids-client 0.0.4 → 0.0.5
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/env.ts +4 -1
- package/src/core/setup.ts +62 -5
- package/src/index.tsx +13 -1
- package/src/render/ink/App.tsx +2 -1
- package/src/render/ink/screens/SetupScreen.tsx +209 -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.5",
|
|
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/env.ts
CHANGED
|
@@ -60,11 +60,14 @@ export function validateEnv(env: KidsClientEnv): { ok: true } | { ok: false; rea
|
|
|
60
60
|
}
|
|
61
61
|
// Accept any supported provider's API key, not just DeepRouter. The
|
|
62
62
|
// setup wizard writes whatever the parent picked into ~/.config/kids-opencode/env
|
|
63
|
-
// which the wrapper sources before exec.
|
|
63
|
+
// which the wrapper sources before exec. KIDS_OAUTH_PROVIDER marks an
|
|
64
|
+
// OAuth flow that opencode handles via its own auth.json store —
|
|
65
|
+
// we trust opencode to gate on actual token validity at serve time.
|
|
64
66
|
const hasAnyKey =
|
|
65
67
|
env.deeprouterApiKey
|
|
66
68
|
|| process.env.ANTHROPIC_API_KEY
|
|
67
69
|
|| process.env.OPENAI_API_KEY
|
|
70
|
+
|| process.env.KIDS_OAUTH_PROVIDER
|
|
68
71
|
if (!env.bypassGateway && !hasAnyKey) {
|
|
69
72
|
return {
|
|
70
73
|
ok: false,
|
package/src/core/setup.ts
CHANGED
|
@@ -16,6 +16,17 @@ import { dirname, join } from "node:path"
|
|
|
16
16
|
|
|
17
17
|
export type ProviderId = "anthropic" | "openai" | "deeprouter"
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Wire protocol between SetupScreen and bin/kids-opencode wrapper:
|
|
21
|
+
* kids-client exits with this code to ask the wrapper to run
|
|
22
|
+
* `opencode auth login --provider <p>` (interactive OAuth that needs the
|
|
23
|
+
* TTY), then re-exec kids-client. See bin/kids-opencode §11 loop.
|
|
24
|
+
*/
|
|
25
|
+
export const OAUTH_HANDOFF_EXIT_CODE = 123
|
|
26
|
+
|
|
27
|
+
/** Providers that support OAuth login via the upstream opencode kernel. */
|
|
28
|
+
export const OAUTH_PROVIDERS: ReadonlyArray<ProviderId> = ["anthropic"] as const
|
|
29
|
+
|
|
19
30
|
export interface ProviderChoice {
|
|
20
31
|
id: ProviderId
|
|
21
32
|
label: string
|
|
@@ -53,10 +64,10 @@ export const PROVIDERS: ProviderChoice[] = [
|
|
|
53
64
|
},
|
|
54
65
|
{
|
|
55
66
|
id: "deeprouter",
|
|
56
|
-
label: "DeepRouter (
|
|
57
|
-
hint: "
|
|
67
|
+
label: "DeepRouter (OpenAI-compatible gateway)",
|
|
68
|
+
hint: "Cheaper than going direct + one key for all models (Anthropic, OpenAI, Google). Built-in kid-safe filters (NSFW + prompt-injection guard). Limited beta — invite-only.",
|
|
58
69
|
envVar: "DEEPROUTER_API_KEY",
|
|
59
|
-
apiKeyUrl: "https://
|
|
70
|
+
apiKeyUrl: "https://deeprouter.ai/",
|
|
60
71
|
config: (env) => ({
|
|
61
72
|
deeprouter: {
|
|
62
73
|
type: "openai-compatible",
|
|
@@ -93,12 +104,54 @@ export function saveSetup(opts: SaveOptions): void {
|
|
|
93
104
|
const envPath = join(opts.configDir, "env")
|
|
94
105
|
const existing = readEnvFile(envPath)
|
|
95
106
|
existing[provider.envVar] = opts.apiKey
|
|
107
|
+
// If the user previously chose OAuth and is now switching to API key, drop
|
|
108
|
+
// the marker so validateEnv doesn't keep routing through OAuth handoff.
|
|
109
|
+
delete existing.KIDS_OAUTH_PROVIDER
|
|
96
110
|
writeEnvFile(envPath, existing)
|
|
97
111
|
|
|
98
112
|
// 2. Rewrite opencode.json provider section.
|
|
99
|
-
|
|
113
|
+
writeOpencodeConfig(opts.configDir, provider, { withApiKey: true })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface SaveOauthOptions {
|
|
117
|
+
configDir: string
|
|
118
|
+
provider: ProviderId
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Stage the OAuth handoff. We write opencode.json without an apiKey block
|
|
123
|
+
* (opencode reads OAuth tokens from its own auth.json), drop any stale
|
|
124
|
+
* provider API keys, and write a KIDS_OAUTH_PROVIDER marker so the
|
|
125
|
+
* wrapper knows which provider to log into. The actual
|
|
126
|
+
* `opencode auth login --provider <p>` invocation happens in
|
|
127
|
+
* bin/kids-opencode after kids-client exits with OAUTH_HANDOFF_EXIT_CODE.
|
|
128
|
+
*/
|
|
129
|
+
export function saveSetupOauth(opts: SaveOauthOptions): void {
|
|
130
|
+
const provider = findProvider(opts.provider)
|
|
131
|
+
ensureConfigDir(opts.configDir)
|
|
132
|
+
|
|
133
|
+
// 1. Update env file: clear this provider's API key (avoid stale leakage),
|
|
134
|
+
// set marker pointing at the OAuth provider.
|
|
135
|
+
const envPath = join(opts.configDir, "env")
|
|
136
|
+
const existing = readEnvFile(envPath)
|
|
137
|
+
delete existing[provider.envVar]
|
|
138
|
+
existing.KIDS_OAUTH_PROVIDER = opts.provider
|
|
139
|
+
writeEnvFile(envPath, existing)
|
|
140
|
+
|
|
141
|
+
// 2. opencode.json without apiKey — opencode uses its auth.json store.
|
|
142
|
+
writeOpencodeConfig(opts.configDir, provider, { withApiKey: false })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function writeOpencodeConfig(
|
|
146
|
+
configDir: string,
|
|
147
|
+
provider: ProviderChoice,
|
|
148
|
+
flags: { withApiKey: boolean },
|
|
149
|
+
): void {
|
|
150
|
+
const configPath = join(configDir, "opencode.json")
|
|
100
151
|
const config = readJsonOrEmpty(configPath)
|
|
101
|
-
config.provider =
|
|
152
|
+
config.provider = flags.withApiKey
|
|
153
|
+
? provider.config(provider.envVar)
|
|
154
|
+
: { [provider.id]: {} }
|
|
102
155
|
config.model = provider.defaultModel
|
|
103
156
|
if (!config.permission) {
|
|
104
157
|
config.permission = {
|
|
@@ -120,11 +173,15 @@ export function saveSetup(opts: SaveOptions): void {
|
|
|
120
173
|
export function hasAnyProviderKey(configDir: string): boolean {
|
|
121
174
|
const env = readEnvFile(join(configDir, "env"))
|
|
122
175
|
if (env.ANTHROPIC_API_KEY || env.OPENAI_API_KEY || env.DEEPROUTER_API_KEY) return true
|
|
176
|
+
// OAuth handoff completed earlier? The marker means opencode has its own
|
|
177
|
+
// auth.json credentials; opencode itself will gate on actual token validity.
|
|
178
|
+
if (env.KIDS_OAUTH_PROVIDER) return true
|
|
123
179
|
// Also accept keys present in the parent shell env (advanced users).
|
|
124
180
|
return !!(
|
|
125
181
|
process.env.ANTHROPIC_API_KEY
|
|
126
182
|
|| process.env.OPENAI_API_KEY
|
|
127
183
|
|| process.env.DEEPROUTER_API_KEY
|
|
184
|
+
|| process.env.KIDS_OAUTH_PROVIDER
|
|
128
185
|
)
|
|
129
186
|
}
|
|
130
187
|
|
package/src/index.tsx
CHANGED
|
@@ -32,7 +32,7 @@ import { readLastSession, writeLastSession } from "./core/last-session.ts"
|
|
|
32
32
|
import { isCompletionTrigger, runCheck } from "./core/check-runner.ts"
|
|
33
33
|
import { App } from "./render/ink/App.tsx"
|
|
34
34
|
import { detectDangerousTopicEn, detectDangerousTopicZh } from "./dangerous-topic-bridge.ts"
|
|
35
|
-
import { saveSetup, type ProviderId } from "./core/setup.ts"
|
|
35
|
+
import { OAUTH_HANDOFF_EXIT_CODE, saveSetup, saveSetupOauth, type ProviderId } from "./core/setup.ts"
|
|
36
36
|
import { reloadEnvFile } from "./core/env-reload.ts"
|
|
37
37
|
import type { InstalledPack } from "./core/course-pack.ts"
|
|
38
38
|
import { loadCoursePack } from "@kidsinai/kids-opencode-plugin"
|
|
@@ -73,6 +73,7 @@ interface AppHandlers {
|
|
|
73
73
|
onSetupSave: (provider: ProviderId, apiKey: string) => Promise<{ ok: true } | { ok: false; reason: string }>
|
|
74
74
|
onSetupContinue: () => Promise<void>
|
|
75
75
|
onSetupSkip: () => void
|
|
76
|
+
onSetupOAuthHandoff: (provider: ProviderId) => Promise<void>
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
async function main(): Promise<void> {
|
|
@@ -197,6 +198,17 @@ function makeHandlers(
|
|
|
197
198
|
const r = getResolveSetup()
|
|
198
199
|
if (r) r()
|
|
199
200
|
},
|
|
201
|
+
onSetupOAuthHandoff: async (provider) => {
|
|
202
|
+
try {
|
|
203
|
+
saveSetupOauth({ configDir: env.configDir, provider })
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.error("kids-client: OAuth handoff prep failed:", err)
|
|
206
|
+
process.exit(1)
|
|
207
|
+
}
|
|
208
|
+
// Hand the TTY to bin/kids-opencode so it can run
|
|
209
|
+
// `opencode auth login --provider <p>` interactively, then re-exec us.
|
|
210
|
+
process.exit(OAUTH_HANDOFF_EXIT_CODE)
|
|
211
|
+
},
|
|
200
212
|
}
|
|
201
213
|
}
|
|
202
214
|
|
package/src/render/ink/App.tsx
CHANGED
|
@@ -43,6 +43,7 @@ export interface AppDeps {
|
|
|
43
43
|
onSetupSave: (provider: ProviderId, apiKey: string) => Promise<{ ok: true } | { ok: false; reason: string }>
|
|
44
44
|
onSetupContinue: () => Promise<void>
|
|
45
45
|
onSetupSkip: () => void
|
|
46
|
+
onSetupOAuthHandoff: (provider: ProviderId) => Promise<void>
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export function App(deps: AppDeps): React.ReactElement {
|
|
@@ -75,7 +76,7 @@ export function App(deps: AppDeps): React.ReactElement {
|
|
|
75
76
|
case "loading":
|
|
76
77
|
return <LoadingScreen locale={deps.locale} message={state.screen.message} />
|
|
77
78
|
case "setup":
|
|
78
|
-
return <SetupScreen locale={deps.locale} onSave={deps.onSetupSave} onContinue={deps.onSetupContinue} onSkip={deps.onSetupSkip} />
|
|
79
|
+
return <SetupScreen locale={deps.locale} onSave={deps.onSetupSave} onContinue={deps.onSetupContinue} onSkip={deps.onSetupSkip} onOAuthHandoff={deps.onSetupOAuthHandoff} />
|
|
79
80
|
case "startup":
|
|
80
81
|
return <StartupScreen locale={deps.locale} coursePack={state.coursePack} onStart={deps.onStart} />
|
|
81
82
|
case "mission":
|
|
@@ -20,6 +20,7 @@ import { KidsLogo } from "../components/KidsLogo.tsx"
|
|
|
20
20
|
import {
|
|
21
21
|
findProvider,
|
|
22
22
|
looksLikeApiKey,
|
|
23
|
+
OAUTH_PROVIDERS,
|
|
23
24
|
PROVIDERS,
|
|
24
25
|
type ProviderId,
|
|
25
26
|
} from "../../../core/setup.ts"
|
|
@@ -30,6 +31,8 @@ type Step =
|
|
|
30
31
|
| "engine_done"
|
|
31
32
|
| "intro"
|
|
32
33
|
| "provider"
|
|
34
|
+
| "auth_choice"
|
|
35
|
+
| "oauth_handoff"
|
|
33
36
|
| "apikey"
|
|
34
37
|
| "saving"
|
|
35
38
|
| "done"
|
|
@@ -42,14 +45,21 @@ interface SetupScreenProps {
|
|
|
42
45
|
onContinue: () => Promise<void>
|
|
43
46
|
/** Skip key — useful for advanced users who set env vars themselves. */
|
|
44
47
|
onSkip: () => void
|
|
48
|
+
/**
|
|
49
|
+
* Hand off to bin/kids-opencode for `opencode auth login --provider <p>`.
|
|
50
|
+
* Implementation writes opencode.json + the KIDS_OAUTH_PROVIDER marker,
|
|
51
|
+
* then process.exit(OAUTH_HANDOFF_EXIT_CODE). Never returns.
|
|
52
|
+
*/
|
|
53
|
+
onOAuthHandoff: (provider: ProviderId) => Promise<void>
|
|
45
54
|
}
|
|
46
55
|
|
|
47
|
-
export function SetupScreen({ locale, onSave, onContinue, onSkip }: SetupScreenProps): React.ReactElement {
|
|
56
|
+
export function SetupScreen({ locale, onSave, onContinue, onSkip, onOAuthHandoff }: SetupScreenProps): React.ReactElement {
|
|
48
57
|
const theme = getTheme()
|
|
49
58
|
const t = STRINGS[locale]
|
|
50
59
|
const initialStep: Step = hasOpencodeBinary() ? "intro" : "engine_install"
|
|
51
60
|
const [step, setStep] = useState<Step>(initialStep)
|
|
52
61
|
const [providerIdx, setProviderIdx] = useState(0)
|
|
62
|
+
const [authChoiceIdx, setAuthChoiceIdx] = useState(0) // 0 = subscription, 1 = api key
|
|
53
63
|
const [apiKey, setApiKey] = useState("")
|
|
54
64
|
const [errorMsg, setErrorMsg] = useState("")
|
|
55
65
|
const [engineLog, setEngineLog] = useState<string[]>([])
|
|
@@ -86,8 +96,54 @@ export function SetupScreen({ locale, onSave, onContinue, onSkip }: SetupScreenP
|
|
|
86
96
|
} else if (step === "provider") {
|
|
87
97
|
if (key.upArrow) setProviderIdx((i) => Math.max(0, i - 1))
|
|
88
98
|
else if (key.downArrow) setProviderIdx((i) => Math.min(PROVIDERS.length - 1, i + 1))
|
|
89
|
-
else if (key.return)
|
|
99
|
+
else if (key.return) {
|
|
100
|
+
// Anthropic supports both Pro/Max subscription OAuth and API key —
|
|
101
|
+
// surface the choice. Other providers go straight to api-key input.
|
|
102
|
+
const picked = PROVIDERS[providerIdx]!
|
|
103
|
+
if (OAUTH_PROVIDERS.includes(picked.id)) {
|
|
104
|
+
setAuthChoiceIdx(0)
|
|
105
|
+
setStep("auth_choice")
|
|
106
|
+
} else {
|
|
107
|
+
setStep("apikey")
|
|
108
|
+
}
|
|
109
|
+
}
|
|
90
110
|
else if (key.escape) setStep("intro")
|
|
111
|
+
} else if (step === "auth_choice") {
|
|
112
|
+
if (key.upArrow) setAuthChoiceIdx((i) => Math.max(0, i - 1))
|
|
113
|
+
else if (key.downArrow) setAuthChoiceIdx((i) => Math.min(1, i + 1))
|
|
114
|
+
else if (key.return) {
|
|
115
|
+
if (authChoiceIdx === 0) {
|
|
116
|
+
// Pro/Max OAuth — render a brief handoff screen, then exit so
|
|
117
|
+
// the wrapper can run `opencode auth login` with full TTY.
|
|
118
|
+
setStep("oauth_handoff")
|
|
119
|
+
// Fire-and-forget — onOAuthHandoff calls process.exit, never returns.
|
|
120
|
+
void onOAuthHandoff(PROVIDERS[providerIdx]!.id)
|
|
121
|
+
} else {
|
|
122
|
+
setStep("apikey")
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (key.escape) setStep("provider")
|
|
126
|
+
} else if (step === "apikey") {
|
|
127
|
+
// Picked the wrong provider? Esc bounces back to the picker.
|
|
128
|
+
// (Enter is consumed by TextInput's onSubmit below, so we only need Esc here.)
|
|
129
|
+
if (key.escape) {
|
|
130
|
+
setApiKey("")
|
|
131
|
+
setStep("provider")
|
|
132
|
+
} else if (
|
|
133
|
+
(input === "d" || input === "D")
|
|
134
|
+
&& provider.id !== "deeprouter"
|
|
135
|
+
&& apiKey === ""
|
|
136
|
+
) {
|
|
137
|
+
// Funnel: parent decides Anthropic/OpenAI billing is too much friction
|
|
138
|
+
// — switch to DeepRouter inline without re-traversing the picker.
|
|
139
|
+
// Guarded by `apiKey === ""` so the keystroke only diverts before
|
|
140
|
+
// the user has started typing; otherwise `d` is just a character.
|
|
141
|
+
const dIdx = PROVIDERS.findIndex((p) => p.id === "deeprouter")
|
|
142
|
+
if (dIdx >= 0) {
|
|
143
|
+
setProviderIdx(dIdx)
|
|
144
|
+
setApiKey("")
|
|
145
|
+
}
|
|
146
|
+
}
|
|
91
147
|
} else if (step === "done") {
|
|
92
148
|
if (key.return) {
|
|
93
149
|
// Inline boot — no exit.
|
|
@@ -207,13 +263,68 @@ export function SetupScreen({ locale, onSave, onContinue, onSkip }: SetupScreenP
|
|
|
207
263
|
)
|
|
208
264
|
}
|
|
209
265
|
|
|
266
|
+
if (step === "auth_choice") {
|
|
267
|
+
const choices = t.authChoice.options
|
|
268
|
+
return (
|
|
269
|
+
<Box flexDirection="column" borderStyle="double" borderColor={theme.accent} paddingX={2} paddingY={1}>
|
|
270
|
+
<Text color={theme.accent} bold>{t.authChoice.title(providerObj.label)}</Text>
|
|
271
|
+
<Box marginTop={1} flexDirection="column">
|
|
272
|
+
{choices.map((c, i) => {
|
|
273
|
+
const active = i === authChoiceIdx
|
|
274
|
+
return (
|
|
275
|
+
<Box key={i}>
|
|
276
|
+
<Text color={active ? theme.kid : theme.fg}>{active ? "▶ " : " "}</Text>
|
|
277
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
278
|
+
<Text color={active ? theme.accent : theme.fg} bold={active}>{c.label}</Text>
|
|
279
|
+
<Text color={theme.fgDim} dimColor={!active}> {c.hint}</Text>
|
|
280
|
+
</Box>
|
|
281
|
+
</Box>
|
|
282
|
+
)
|
|
283
|
+
})}
|
|
284
|
+
</Box>
|
|
285
|
+
<Box marginTop={1}>
|
|
286
|
+
<Text color={theme.accent}>{t.authChoice.keys}</Text>
|
|
287
|
+
</Box>
|
|
288
|
+
</Box>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (step === "oauth_handoff") {
|
|
293
|
+
return (
|
|
294
|
+
<Box flexDirection="column" alignItems="center" paddingY={1}>
|
|
295
|
+
<KidsLogo />
|
|
296
|
+
<Box marginTop={2} borderStyle="round" borderColor={theme.accent} paddingX={2} paddingY={1} flexDirection="column">
|
|
297
|
+
<Box>
|
|
298
|
+
<Text color={theme.accent}>
|
|
299
|
+
<Spinner type="dots" />
|
|
300
|
+
</Text>
|
|
301
|
+
<Text color={theme.accent} bold> {t.oauthHandoff.title}</Text>
|
|
302
|
+
</Box>
|
|
303
|
+
<Box marginTop={1}>
|
|
304
|
+
<Text color={theme.fgDim}>{t.oauthHandoff.line1}</Text>
|
|
305
|
+
</Box>
|
|
306
|
+
<Box>
|
|
307
|
+
<Text color={theme.fgDim}>{t.oauthHandoff.line2}</Text>
|
|
308
|
+
</Box>
|
|
309
|
+
</Box>
|
|
310
|
+
</Box>
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
210
314
|
if (step === "apikey") {
|
|
315
|
+
const steps = t.providerSteps[provider.id]
|
|
211
316
|
return (
|
|
212
317
|
<Box flexDirection="column" borderStyle="double" borderColor={theme.accent} paddingX={2} paddingY={1}>
|
|
213
318
|
<Text color={theme.accent} bold>{t.apiKeyTitle(providerObj.label)}</Text>
|
|
214
319
|
<Box marginTop={1} flexDirection="column">
|
|
215
320
|
<Text color={theme.fgDim}>{t.apiKeyHint(providerObj.apiKeyUrl)}</Text>
|
|
216
321
|
</Box>
|
|
322
|
+
<Box marginTop={1} flexDirection="column" paddingX={1}>
|
|
323
|
+
<Text color={theme.accent}>{t.stepsHeader}</Text>
|
|
324
|
+
{steps.map((line, i) => (
|
|
325
|
+
<Text key={i} color={theme.fgDim}> {line}</Text>
|
|
326
|
+
))}
|
|
327
|
+
</Box>
|
|
217
328
|
<Box marginTop={1}>
|
|
218
329
|
<Text color={theme.kid}>🔑 </Text>
|
|
219
330
|
<TextInput
|
|
@@ -243,6 +354,14 @@ export function SetupScreen({ locale, onSave, onContinue, onSkip }: SetupScreenP
|
|
|
243
354
|
<Box marginTop={1}>
|
|
244
355
|
<Text color={theme.fgDim}>{t.apiKeyEnter}</Text>
|
|
245
356
|
</Box>
|
|
357
|
+
<Box>
|
|
358
|
+
<Text color={theme.fgDim}>{t.apiKeyBack}</Text>
|
|
359
|
+
</Box>
|
|
360
|
+
{provider.id !== "deeprouter" && (
|
|
361
|
+
<Box>
|
|
362
|
+
<Text color={theme.accent}>{t.apiKeyToDR}</Text>
|
|
363
|
+
</Box>
|
|
364
|
+
)}
|
|
246
365
|
</Box>
|
|
247
366
|
)
|
|
248
367
|
}
|
|
@@ -303,11 +422,55 @@ const STRINGS = {
|
|
|
303
422
|
providerTitle: "选一个 AI 服务",
|
|
304
423
|
providerKeys: "[↑↓] 选 · [Enter] 下一步 · [Esc] 返回",
|
|
305
424
|
getKey: "去拿 key",
|
|
425
|
+
authChoice: {
|
|
426
|
+
title: (label: string) => `${label} 怎么用?`,
|
|
427
|
+
keys: "[↑↓] 选 · [Enter] 确认 · [Esc] 返回",
|
|
428
|
+
options: [
|
|
429
|
+
{
|
|
430
|
+
label: "用我的 Claude Pro/Max 订阅(推荐)",
|
|
431
|
+
hint: "不用 API key、不用充值;用现有 claude.ai 账号一键登录",
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
label: "用 API key(按量计费 ~$5/月)",
|
|
435
|
+
hint: "公司账号、没订阅、或想分账时选这个",
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
},
|
|
439
|
+
oauthHandoff: {
|
|
440
|
+
title: "正在让 Claude 登录接管屏幕…",
|
|
441
|
+
line1: "马上会跳出浏览器让你登录 claude.ai 账号。",
|
|
442
|
+
line2: "登录完后我会自动接回来,给孩子继续。",
|
|
443
|
+
},
|
|
306
444
|
apiKeyTitle: (label: string) => `输入 ${label} 的 API key`,
|
|
307
445
|
apiKeyHint: (url: string) => `没 key?打开浏览器:${url}`,
|
|
308
446
|
apiKeyPlaceholder: (env: string) => `${env}(粘进来后按 Enter)`,
|
|
309
447
|
apiKeyEnter: "[Enter] 保存 · 你的 key 只存在本地",
|
|
448
|
+
apiKeyBack: "[Esc] 选错了?回去重选",
|
|
449
|
+
apiKeyToDR: "[d] 不想充值?改用 DeepRouter — 无需信用卡(输入前按)",
|
|
310
450
|
apiKeyInvalid: (env: string) => `这看起来不是有效的 ${env}。再试一次。`,
|
|
451
|
+
stepsHeader: "在哪里点开:",
|
|
452
|
+
providerSteps: {
|
|
453
|
+
anthropic: [
|
|
454
|
+
"1. 打开浏览器 → console.anthropic.com",
|
|
455
|
+
"2. 用 Google 账号登录,或邮箱注册(免费)",
|
|
456
|
+
"3. 右上角点头像 → Billing → Add credits → 充 $5 起(信用卡 / Apple Pay)",
|
|
457
|
+
"4. 左侧菜单点 API Keys → 按 \"Create Key\" 按钮 → 起个名(比如 kids)",
|
|
458
|
+
"5. 弹窗里复制 sk-ant- 开头的整串,回到这里粘上",
|
|
459
|
+
],
|
|
460
|
+
openai: [
|
|
461
|
+
"1. 打开浏览器 → platform.openai.com/api-keys",
|
|
462
|
+
"2. 用 Google 账号登录,或邮箱注册",
|
|
463
|
+
"3. 左侧 Billing → Add to credit balance → 充 $5 起(信用卡)",
|
|
464
|
+
"4. 回到 API Keys → 按 \"+ Create new secret key\" → 起个名(比如 kids)",
|
|
465
|
+
"5. 弹窗里复制 sk- 开头的整串(只显示一次!),回来粘上",
|
|
466
|
+
],
|
|
467
|
+
deeprouter: [
|
|
468
|
+
"1. 打开浏览器 → deeprouter.ai(目前内测,需要邀请码)",
|
|
469
|
+
"2. 拿邀请码注册账号",
|
|
470
|
+
"3. 控制台 → API Keys → 创建新 key",
|
|
471
|
+
"4. 复制 key,回到这里粘上",
|
|
472
|
+
],
|
|
473
|
+
},
|
|
311
474
|
saving: "保存中…",
|
|
312
475
|
errTitle: "出了点问题",
|
|
313
476
|
errRetry: "[Enter] 再试",
|
|
@@ -328,11 +491,55 @@ const STRINGS = {
|
|
|
328
491
|
providerTitle: "Pick an AI service",
|
|
329
492
|
providerKeys: "[↑↓] choose · [Enter] next · [Esc] back",
|
|
330
493
|
getKey: "Get key at",
|
|
494
|
+
authChoice: {
|
|
495
|
+
title: (label: string) => `How will you connect to ${label}?`,
|
|
496
|
+
keys: "[↑↓] choose · [Enter] confirm · [Esc] back",
|
|
497
|
+
options: [
|
|
498
|
+
{
|
|
499
|
+
label: "Use my Claude Pro/Max subscription (recommended)",
|
|
500
|
+
hint: "No API key, no top-up — sign in with your existing claude.ai account",
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
label: "Use an API key (pay-as-you-go ~$5/month)",
|
|
504
|
+
hint: "Pick this for company accounts, no subscription, or separate billing",
|
|
505
|
+
},
|
|
506
|
+
],
|
|
507
|
+
},
|
|
508
|
+
oauthHandoff: {
|
|
509
|
+
title: "Handing off to Claude login…",
|
|
510
|
+
line1: "A browser window will open to sign in to your claude.ai account.",
|
|
511
|
+
line2: "I'll pick back up automatically once you're done.",
|
|
512
|
+
},
|
|
331
513
|
apiKeyTitle: (label: string) => `Enter your ${label} API key`,
|
|
332
514
|
apiKeyHint: (url: string) => `Don't have a key yet? Open: ${url}`,
|
|
333
515
|
apiKeyPlaceholder: (env: string) => `${env} (paste then Enter)`,
|
|
334
516
|
apiKeyEnter: "[Enter] save · Your key stays on this machine.",
|
|
517
|
+
apiKeyBack: "[Esc] Picked wrong one? Go back and re-pick.",
|
|
518
|
+
apiKeyToDR: "[d] Skip the billing — use DeepRouter instead (no credit card). Press before typing.",
|
|
335
519
|
apiKeyInvalid: (env: string) => `That doesn't look like a valid ${env}. Try again.`,
|
|
520
|
+
stepsHeader: "Where to click:",
|
|
521
|
+
providerSteps: {
|
|
522
|
+
anthropic: [
|
|
523
|
+
"1. Open in browser → console.anthropic.com",
|
|
524
|
+
"2. Sign in with Google, or sign up with email (free)",
|
|
525
|
+
"3. Top-right profile → Billing → Add credits → top up $5+ (card / Apple Pay)",
|
|
526
|
+
"4. Left menu → API Keys → \"Create Key\" → name it (e.g. kids)",
|
|
527
|
+
"5. Copy the sk-ant-… string from the popup, paste it here",
|
|
528
|
+
],
|
|
529
|
+
openai: [
|
|
530
|
+
"1. Open in browser → platform.openai.com/api-keys",
|
|
531
|
+
"2. Sign in with Google, or sign up with email",
|
|
532
|
+
"3. Left menu → Billing → Add to credit balance → top up $5+ (card)",
|
|
533
|
+
"4. Back to API Keys → \"+ Create new secret key\" → name it (e.g. kids)",
|
|
534
|
+
"5. Copy the sk- string from the popup (shown only once!), paste it here",
|
|
535
|
+
],
|
|
536
|
+
deeprouter: [
|
|
537
|
+
"1. Open in browser → deeprouter.ai (closed beta — invite code required)",
|
|
538
|
+
"2. Sign up with the invite code",
|
|
539
|
+
"3. Dashboard → API Keys → Create new key",
|
|
540
|
+
"4. Copy the key, paste it here",
|
|
541
|
+
],
|
|
542
|
+
},
|
|
336
543
|
saving: "Saving…",
|
|
337
544
|
errTitle: "Something went wrong",
|
|
338
545
|
errRetry: "[Enter] Try again",
|