From 99ad53a1ee724680ac4dc980bfc7a777f5e2ace7 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 15 Feb 2026 20:06:35 -0800 Subject: [PATCH] Add API vs Coding Plan mode selection for Z.AI auth --- docs/plans/state.json | 13 +++++++++++ src/cli/zai-auth.test.ts | 22 ++++++++++++++++++- src/cli/zai-auth.ts | 42 ++++++++++++++++++++++++++++++++++-- src/frontends/tui/minimal.ts | 11 +++++++++- 4 files changed, 84 insertions(+), 4 deletions(-) diff --git a/docs/plans/state.json b/docs/plans/state.json index 4047f12..9cc9b68 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -110,6 +110,19 @@ "test_status": "pnpm test:run src/cli/suppressNodeWarnings.test.ts src/cli/index.test.ts + pnpm typecheck + pnpm build passing" }, + "zai-auth-mode-selection-api-vs-plan": { + "status": "completed", + "date": "2026-02-16", + "updated": "2026-02-16", + "summary": "Added explicit Z.AI mode selection (API vs Coding Plan) to both `zai-auth` CLI and TUI `/login zai` flow. CLI now supports `--mode api|plan` for non-interactive use and prompts when omitted. Post-auth guidance now shows the mode-specific endpoint directly.", + "files_modified": [ + "src/cli/zai-auth.ts", + "src/cli/zai-auth.test.ts", + "src/frontends/tui/minimal.ts" + ], + "test_status": "pnpm test:run src/cli/zai-auth.test.ts src/frontends/tui/minimal.test.ts + pnpm typecheck passing" + }, + "deployment-port-env-override": { "status": "completed", "date": "2026-02-16", diff --git a/src/cli/zai-auth.test.ts b/src/cli/zai-auth.test.ts index e2f5857..5a19997 100644 --- a/src/cli/zai-auth.test.ts +++ b/src/cli/zai-auth.test.ts @@ -60,7 +60,7 @@ describe('zai-auth command', () => { it('re-prompts and stores new key when credential exists and user answers yes', async () => { mockLoadStoredZaiAuth.mockReturnValue({ api_key: 'existing-key', created_at: '2026-02-16T00:00:00.000Z' }); - mockReadlineAnswers(['y', 'new-zai-key']); + mockReadlineAnswers(['y', '1', 'new-zai-key']); const program = new Command(); const { registerZaiAuthCommand } = await import('./zai-auth.js'); @@ -77,4 +77,24 @@ describe('zai-auth command', () => { consoleLog.mockRestore(); consoleError.mockRestore(); }); + + it('supports --mode plan without interactive mode selection', async () => { + mockLoadStoredZaiAuth.mockReturnValue(null); + mockReadlineAnswers(['new-zai-plan-key']); + + const program = new Command(); + const { registerZaiAuthCommand } = await import('./zai-auth.js'); + registerZaiAuthCommand(program); + + const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined); + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined); + + await program.parseAsync(['node', 'test', 'zai-auth', '--mode', 'plan']); + + expect(mockStoreZaiAuth).toHaveBeenCalledWith('new-zai-plan-key'); + expect(consoleError).not.toHaveBeenCalled(); + + consoleLog.mockRestore(); + consoleError.mockRestore(); + }); }); diff --git a/src/cli/zai-auth.ts b/src/cli/zai-auth.ts index 7e59e97..918a010 100644 --- a/src/cli/zai-auth.ts +++ b/src/cli/zai-auth.ts @@ -2,6 +2,8 @@ import type { Command } from 'commander'; import readline from 'readline'; import { loadStoredZaiAuth, storeZaiAuth } from '../auth/index.js'; +type ZaiAuthMode = 'api' | 'plan'; + async function promptHidden(question: string): Promise { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true }); const rlAny = rl as unknown as { stdoutMuted?: boolean; _writeToOutput?: (s: string) => void }; @@ -35,11 +37,39 @@ async function promptYesNo(question: string): Promise { return normalized === 'y' || normalized === 'yes'; } +async function promptText(question: string): Promise { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true }); + const answer = await new Promise((resolve) => rl.question(question, resolve)); + rl.close(); + return answer.trim(); +} + +function parseZaiAuthMode(value: string): ZaiAuthMode { + const mode = value.trim().toLowerCase(); + if (mode === 'api' || mode === 'plan') { + return mode; + } + throw new Error(`Invalid mode "${value}". Expected: api or plan.`); +} + +async function resolveZaiAuthMode(mode?: ZaiAuthMode): Promise { + if (mode) { + return mode; + } + + console.log('Choose Z.AI auth mode:'); + console.log(' 1) API (standard API endpoint)'); + console.log(' 2) Coding Plan (coding endpoint)'); + const choice = (await promptText('Select [1-2] (default 1): ')).toLowerCase(); + return choice === '2' || choice === 'plan' ? 'plan' : 'api'; +} + export function registerZaiAuthCommand(program: Command): void { program .command('zai-auth') .description('Store a Z.AI API key for the zhipuai provider (auth.json)') - .action(async () => { + .option('--mode ', 'Credential mode: api or plan', parseZaiAuthMode) + .action(async (opts: { mode?: ZaiAuthMode }) => { const existing = loadStoredZaiAuth(); if (existing) { console.log('Z.AI credential already exists.'); @@ -50,6 +80,8 @@ export function registerZaiAuthCommand(program: Command): void { } } + const mode = await resolveZaiAuthMode(opts.mode); + console.log('Z.AI uses API keys (HTTP Bearer), not an OAuth device flow.'); console.log('Create a key at: https://z.ai/manage-apikey/apikey-list'); console.log(''); @@ -60,7 +92,13 @@ export function registerZaiAuthCommand(program: Command): void { console.log(''); console.log('Z.AI credential stored in ~/.config/flynn/auth.json'); console.log(''); - console.log('Tip: For GLM Coding Plan set model endpoint to: https://api.z.ai/api/coding/paas/v4'); + if (mode === 'plan') { + console.log('Mode: Coding Plan'); + console.log('Set model endpoint to: https://api.z.ai/api/coding/paas/v4'); + } else { + console.log('Mode: API'); + console.log('Set model endpoint to: https://api.z.ai/api/paas/v4'); + } } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(`Z.AI auth failed: ${message}`); diff --git a/src/frontends/tui/minimal.ts b/src/frontends/tui/minimal.ts index 20ad479..ed3bd08 100644 --- a/src/frontends/tui/minimal.ts +++ b/src/frontends/tui/minimal.ts @@ -620,6 +620,9 @@ export class MinimalTui { console.log(`${colors.gray}Z.AI uses API keys (HTTP Bearer), not an OAuth device flow.${colors.reset}`); console.log(`${colors.gray}Create a key at:${colors.reset} https://z.ai/manage-apikey/apikey-list`); + console.log(`${colors.gray}Choose mode: 1) API 2) Coding Plan${colors.reset}`); + const choice = (await this.prompt(`${colors.orange}Select [1-2] (default 1):${colors.reset} `)).trim().toLowerCase(); + const mode = (choice === '2' || choice === 'plan') ? 'plan' : 'api'; console.log(''); try { @@ -628,7 +631,13 @@ export class MinimalTui { storeZaiAuth(apiKey); console.log(''); console.log(`${colors.gray}Z.AI credential stored in ~/.config/flynn/auth.json${colors.reset}`); - console.log(`${colors.gray}Tip: For GLM Coding Plan set endpoint to https://api.z.ai/api/coding/paas/v4${colors.reset}\n`); + if (mode === 'plan') { + console.log(`${colors.gray}Mode: Coding Plan${colors.reset}`); + console.log(`${colors.gray}Set endpoint to https://api.z.ai/api/coding/paas/v4${colors.reset}\n`); + } else { + console.log(`${colors.gray}Mode: API${colors.reset}`); + console.log(`${colors.gray}Set endpoint to https://api.z.ai/api/paas/v4${colors.reset}\n`); + } } catch (error) { const message = error instanceof Error ? error.message : String(error); console.log(`${colors.gray}Z.AI auth failed:${colors.reset} ${message}\n`);