diff --git a/README.md b/README.md index 00a8879..efdf16a 100644 --- a/README.md +++ b/README.md @@ -97,17 +97,18 @@ Run `zerogpu --help` for command-level help. #### `login` -Sign in to ZeroGPU. Prompts for your API key (masked input) and project ID, validates them, and persists the credentials. +Sign in to ZeroGPU. Prompts for your API key (masked input), validates it, and persists the credential. The project is derived from the API key; pass `--project-id` only if you want to pin a specific project. ```bash zerogpu login +zerogpu login --api-key zgpu-api-xxxxxxxx zerogpu login --api-key zgpu-api-xxxxxxxx --project-id 4ed3e5bb-c2ed-4d4a-8a66-2b161a27fd1a ``` | Option | Description | |---|---| | `--api-key ` | Provide the API key directly (skips the prompt). | -| `--project-id ` | Provide the project ID directly (skips the prompt). | +| `--project-id ` | _(optional)_ Pin a specific project ID; derived from the API key if omitted. | #### `status` diff --git a/docs/ADDING_COMMANDS.md b/docs/ADDING_COMMANDS.md index 29ce337..3677062 100644 --- a/docs/ADDING_COMMANDS.md +++ b/docs/ADDING_COMMANDS.md @@ -22,14 +22,19 @@ This guide explains how to add a new CLI command to the ZeroGPU CLI. - Add the import alongside the other `register...` imports. - Call `registerXxxCommand(program)` inside `buildProgram()`. -3. **Auth-gated commands** should follow this pattern: +3. **Auth-gated commands** should follow this pattern. Only the API key is + required; the project is derived from it. Send `x-project-id` only when a + project ID is configured: ```ts const apiKey = getApiKey(); - const projectId = getProjectId(); - if (!apiKey || !projectId) { + const projectId = getProjectId(); // optional + if (!apiKey) { console.error("You're not fully signed in yet. Run 'zerogpu login' ..."); process.exit(1); } + // headers: + // "x-api-key": apiKey.apiKey, + // ...(projectId ? { "x-project-id": projectId.projectId } : {}), ``` 4. **Error handling**: diff --git a/docs/DOCUMENTATION.md b/docs/DOCUMENTATION.md index 4a95a75..a8e830a 100644 --- a/docs/DOCUMENTATION.md +++ b/docs/DOCUMENTATION.md @@ -75,10 +75,10 @@ Credentials are persisted to a local config file and `ZEROGPU_API_KEY` is added | Variable | Purpose | |---|---| | `ZEROGPU_API_KEY` | API key. Used as fallback if no config file is present. Written by `zerogpu login`. | -| `ZEROGPU_PROJECT_ID` | Project ID. Used as fallback if no config file is present. | +| `ZEROGPU_PROJECT_ID` | _(optional)_ Project ID. Used as fallback if no config file is present. The backend derives the project from the API key, so this is only needed to pin a specific project. | ### Resolution order -For every request the CLI resolves credentials by checking the **config file first**, then the corresponding **environment variable**. If either credential is missing, the command exits with code `1` and prompts you to run `zerogpu login`. +For every request the CLI resolves credentials by checking the **config file first**, then the corresponding **environment variable**. If the API key is missing, the command exits with code `1` and prompts you to run `zerogpu login`. The project ID is optional and sent (as `x-project-id`) only when present. --- @@ -128,7 +128,7 @@ zerogpu login [--api-key ] [--project-id ] | Flag | Type | Required | Description | |---|---|---|---| | `--api-key ` | string | optional | Provide the API key non-interactively. Must start with `zgpu-api-`. If omitted, the CLI shows a masked prompt. | -| `--project-id ` | string (UUID) | optional | Provide the Project ID non-interactively. Must be a valid UUID v4 string. If omitted, the CLI prompts plainly. | +| `--project-id ` | string (UUID) | optional | Pin a specific Project ID. Must be a valid UUID v4 string. If omitted, the project is derived from the API key (the CLI does not prompt for it). | **Examples** ```bash @@ -624,7 +624,7 @@ All inference commands POST to: POST https://api.zerogpu.ai/v1/responses Content-Type: application/json x-api-key: -x-project-id: +x-project-id: # optional; sent only when a project is configured ``` The request body is: @@ -663,7 +663,7 @@ It picks the first `content` entry whose `type === "output_text"` (falling back | Symptom | Likely cause | Fix | |---|---|---| -| `You're not fully signed in yet.` | Missing config + missing env var | Run `zerogpu login`, or export `ZEROGPU_API_KEY` and `ZEROGPU_PROJECT_ID`. | +| `You're not fully signed in yet.` | Missing config + missing env var | Run `zerogpu login`, or export `ZEROGPU_API_KEY` (`ZEROGPU_PROJECT_ID` is optional). | | `That doesn't look like a valid API key` | Key doesn't start with `zgpu-api-` | Copy the full key from the ZeroGPU console. | | `Project ID must be a UUID.` | Pasted a name/slug | Use the UUID shown next to the project in the console. | | `Request failed with status 401` | Bad/revoked key or wrong project | Re-run `zerogpu login`. | diff --git a/src/commands/chat.ts b/src/commands/chat.ts index 2af1e70..d588812 100644 --- a/src/commands/chat.ts +++ b/src/commands/chat.ts @@ -21,9 +21,9 @@ export function registerChatCommand(program: Command): void { const apiKey = getApiKey(); const projectId = getProjectId(); - if (!apiKey || !projectId) { + if (!apiKey) { console.error( - "You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.", + "You're not fully signed in yet. Run 'zerogpu login' to set your API key.", ); process.exit(1); } @@ -41,7 +41,7 @@ export function registerChatCommand(program: Command): void { headers: { "content-type": "application/json", "x-api-key": apiKey.apiKey, - "x-project-id": projectId.projectId, + ...(projectId ? { "x-project-id": projectId.projectId } : {}), }, body: JSON.stringify(body), }); diff --git a/src/commands/chatThinking.ts b/src/commands/chatThinking.ts index b448d8b..d59d70c 100644 --- a/src/commands/chatThinking.ts +++ b/src/commands/chatThinking.ts @@ -19,9 +19,9 @@ export function registerChatThinkingCommand(program: Command): void { const apiKey = getApiKey(); const projectId = getProjectId(); - if (!apiKey || !projectId) { + if (!apiKey) { console.error( - "You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.", + "You're not fully signed in yet. Run 'zerogpu login' to set your API key.", ); process.exit(1); } @@ -33,7 +33,7 @@ export function registerChatThinkingCommand(program: Command): void { headers: { "content-type": "application/json", "x-api-key": apiKey.apiKey, - "x-project-id": projectId.projectId, + ...(projectId ? { "x-project-id": projectId.projectId } : {}), }, body: JSON.stringify({ model: MODEL, diff --git a/src/commands/classifyIab.ts b/src/commands/classifyIab.ts index 19eaf17..12efcde 100644 --- a/src/commands/classifyIab.ts +++ b/src/commands/classifyIab.ts @@ -19,9 +19,9 @@ export function registerClassifyIabCommand(program: Command): void { const apiKey = getApiKey(); const projectId = getProjectId(); - if (!apiKey || !projectId) { + if (!apiKey) { console.error( - "You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.", + "You're not fully signed in yet. Run 'zerogpu login' to set your API key.", ); process.exit(1); } @@ -33,7 +33,7 @@ export function registerClassifyIabCommand(program: Command): void { headers: { "content-type": "application/json", "x-api-key": apiKey.apiKey, - "x-project-id": projectId.projectId, + ...(projectId ? { "x-project-id": projectId.projectId } : {}), }, body: JSON.stringify({ model: MODEL, diff --git a/src/commands/classifyIabEnriched.ts b/src/commands/classifyIabEnriched.ts index 784b9a8..7c6fdfd 100644 --- a/src/commands/classifyIabEnriched.ts +++ b/src/commands/classifyIabEnriched.ts @@ -19,9 +19,9 @@ export function registerClassifyIabEnrichedCommand(program: Command): void { const apiKey = getApiKey(); const projectId = getProjectId(); - if (!apiKey || !projectId) { + if (!apiKey) { console.error( - "You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.", + "You're not fully signed in yet. Run 'zerogpu login' to set your API key.", ); process.exit(1); } @@ -33,7 +33,7 @@ export function registerClassifyIabEnrichedCommand(program: Command): void { headers: { "content-type": "application/json", "x-api-key": apiKey.apiKey, - "x-project-id": projectId.projectId, + ...(projectId ? { "x-project-id": projectId.projectId } : {}), }, body: JSON.stringify({ model: MODEL, diff --git a/src/commands/classifyStructured.ts b/src/commands/classifyStructured.ts index 7649f2e..9755fd1 100644 --- a/src/commands/classifyStructured.ts +++ b/src/commands/classifyStructured.ts @@ -23,9 +23,9 @@ export function registerClassifyStructuredCommand(program: Command): void { const apiKey = getApiKey(); const projectId = getProjectId(); - if (!apiKey || !projectId) { + if (!apiKey) { console.error( - "You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.", + "You're not fully signed in yet. Run 'zerogpu login' to set your API key.", ); process.exit(1); } @@ -46,7 +46,7 @@ export function registerClassifyStructuredCommand(program: Command): void { headers: { "content-type": "application/json", "x-api-key": apiKey.apiKey, - "x-project-id": projectId.projectId, + ...(projectId ? { "x-project-id": projectId.projectId } : {}), }, body: JSON.stringify({ model: MODEL, diff --git a/src/commands/classifyZeroShot.ts b/src/commands/classifyZeroShot.ts index 5175e8c..6ffcdce 100644 --- a/src/commands/classifyZeroShot.ts +++ b/src/commands/classifyZeroShot.ts @@ -37,9 +37,9 @@ export function registerClassifyZeroShotCommand(program: Command): void { const apiKey = getApiKey(); const projectId = getProjectId(); - if (!apiKey || !projectId) { + if (!apiKey) { console.error( - "You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.", + "You're not fully signed in yet. Run 'zerogpu login' to set your API key.", ); process.exit(1); } @@ -66,7 +66,7 @@ export function registerClassifyZeroShotCommand(program: Command): void { headers: { "content-type": "application/json", "x-api-key": apiKey.apiKey, - "x-project-id": projectId.projectId, + ...(projectId ? { "x-project-id": projectId.projectId } : {}), }, body: JSON.stringify({ model: MODEL, diff --git a/src/commands/extractEntities.ts b/src/commands/extractEntities.ts index a18b2f4..22e8035 100644 --- a/src/commands/extractEntities.ts +++ b/src/commands/extractEntities.ts @@ -43,9 +43,9 @@ export function registerExtractEntitiesCommand(program: Command): void { const apiKey = getApiKey(); const projectId = getProjectId(); - if (!apiKey || !projectId) { + if (!apiKey) { console.error( - "You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.", + "You're not fully signed in yet. Run 'zerogpu login' to set your API key.", ); process.exit(1); } @@ -76,7 +76,7 @@ export function registerExtractEntitiesCommand(program: Command): void { headers: { "content-type": "application/json", "x-api-key": apiKey.apiKey, - "x-project-id": projectId.projectId, + ...(projectId ? { "x-project-id": projectId.projectId } : {}), }, body: JSON.stringify({ model: MODEL, diff --git a/src/commands/extractJson.ts b/src/commands/extractJson.ts index 7e55885..6784f38 100644 --- a/src/commands/extractJson.ts +++ b/src/commands/extractJson.ts @@ -23,9 +23,9 @@ export function registerExtractJsonCommand(program: Command): void { const apiKey = getApiKey(); const projectId = getProjectId(); - if (!apiKey || !projectId) { + if (!apiKey) { console.error( - "You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.", + "You're not fully signed in yet. Run 'zerogpu login' to set your API key.", ); process.exit(1); } @@ -46,7 +46,7 @@ export function registerExtractJsonCommand(program: Command): void { headers: { "content-type": "application/json", "x-api-key": apiKey.apiKey, - "x-project-id": projectId.projectId, + ...(projectId ? { "x-project-id": projectId.projectId } : {}), }, body: JSON.stringify({ model: MODEL, diff --git a/src/commands/extractPii.ts b/src/commands/extractPii.ts index ca90e75..9d3ba9e 100644 --- a/src/commands/extractPii.ts +++ b/src/commands/extractPii.ts @@ -35,9 +35,9 @@ export function registerExtractPiiCommand(program: Command): void { const apiKey = getApiKey(); const projectId = getProjectId(); - if (!apiKey || !projectId) { + if (!apiKey) { console.error( - "You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.", + "You're not fully signed in yet. Run 'zerogpu login' to set your API key.", ); process.exit(1); } @@ -59,7 +59,7 @@ export function registerExtractPiiCommand(program: Command): void { headers: { "content-type": "application/json", "x-api-key": apiKey.apiKey, - "x-project-id": projectId.projectId, + ...(projectId ? { "x-project-id": projectId.projectId } : {}), }, body: JSON.stringify({ model: MODEL, diff --git a/src/commands/generateFollowups.ts b/src/commands/generateFollowups.ts index d104847..84a68a9 100644 --- a/src/commands/generateFollowups.ts +++ b/src/commands/generateFollowups.ts @@ -19,9 +19,9 @@ export function registerGenerateFollowupsCommand(program: Command): void { const apiKey = getApiKey(); const projectId = getProjectId(); - if (!apiKey || !projectId) { + if (!apiKey) { console.error( - "You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.", + "You're not fully signed in yet. Run 'zerogpu login' to set your API key.", ); process.exit(1); } @@ -33,7 +33,7 @@ export function registerGenerateFollowupsCommand(program: Command): void { headers: { "content-type": "application/json", "x-api-key": apiKey.apiKey, - "x-project-id": projectId.projectId, + ...(projectId ? { "x-project-id": projectId.projectId } : {}), }, body: JSON.stringify({ model: MODEL, diff --git a/src/commands/login.ts b/src/commands/login.ts index 6d56e31..ccde69f 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -2,7 +2,7 @@ import { spawn } from "node:child_process"; import { Command } from "commander"; import { validateApiKey, validateProjectId } from "../lib/auth.js"; import { writeConfig, readConfig } from "../lib/config.js"; -import { promptMasked, promptPlain } from "../lib/prompt.js"; +import { promptMasked } from "../lib/prompt.js"; import { upsertEnvExport } from "../lib/shellEnv.js"; interface LoginOptions { @@ -49,21 +49,21 @@ export function registerLoginCommand(program: Command): void { .option("--api-key ", "Provide your API key directly (skips the prompt).") .option( "--project-id ", - "Provide your ZeroGPU project ID directly (skips the prompt).", + "Optionally pin a ZeroGPU project ID. If omitted, the project is derived from your API key.", ) .action(async (options: LoginOptions) => { let rawKey = options.apiKey; - let rawProjectId = options.projectId; + const rawProjectId = options.projectId; - if (!rawKey || !rawProjectId) { + if (!rawKey) { const opened = await openBrowser(DASHBOARD_URL); if (opened) { console.log( - `Opening ${DASHBOARD_URL} in your browser — grab your Project ID and API Key from there.`, + `Opening ${DASHBOARD_URL} in your browser — grab your API Key from there.`, ); } else { console.log( - `Tip: open ${DASHBOARD_URL} in your browser to grab your Project ID and API Key.`, + `Tip: open ${DASHBOARD_URL} in your browser to grab your API Key.`, ); } } @@ -84,32 +84,34 @@ export function registerLoginCommand(program: Command): void { process.exit(1); } - if (!rawProjectId) { - try { - rawProjectId = await promptPlain("Please enter your ZeroGPU project ID: "); - } catch { - console.error("Login cancelled. No changes were made."); + // Project ID is optional — the backend derives the project from the API key. + // Validate and store it only when the caller explicitly provided one. + let projectId: string | undefined; + if (rawProjectId) { + const projectResult = validateProjectId(rawProjectId); + if (!projectResult.ok) { + console.error( + `That doesn't look like a valid project ID — ${projectResult.reason}`, + ); + console.error( + "It should be a UUID like 4ed3e5bb-c2ed-4d4a-8a66-2b161a27fd1a. Please try again.", + ); process.exit(1); } - } - - const projectResult = validateProjectId(rawProjectId ?? ""); - if (!projectResult.ok) { - console.error( - `That doesn't look like a valid project ID — ${projectResult.reason}`, - ); - console.error( - "It should be a UUID like 4ed3e5bb-c2ed-4d4a-8a66-2b161a27fd1a. Please try again.", - ); - process.exit(1); + projectId = projectResult.key; } const existing = readConfig(); - writeConfig({ - ...existing, - apiKey: result.key, - projectId: projectResult.key, - }); + // Re-login clears any previously pinned project ID; it is restored only + // when --project-id is passed this run, so login without it returns to the + // unpinned (derive-from-key) state. + const next = { ...existing, apiKey: result.key }; + if (projectId) { + next.projectId = projectId; + } else { + delete next.projectId; + } + writeConfig(next); const env = upsertEnvExport("ZEROGPU_API_KEY", result.key); process.env["ZEROGPU_API_KEY"] = result.key; diff --git a/src/commands/redactPii.ts b/src/commands/redactPii.ts index ab5e8ea..a7e0062 100644 --- a/src/commands/redactPii.ts +++ b/src/commands/redactPii.ts @@ -19,9 +19,9 @@ export function registerRedactPiiCommand(program: Command): void { const apiKey = getApiKey(); const projectId = getProjectId(); - if (!apiKey || !projectId) { + if (!apiKey) { console.error( - "You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.", + "You're not fully signed in yet. Run 'zerogpu login' to set your API key.", ); process.exit(1); } @@ -33,7 +33,7 @@ export function registerRedactPiiCommand(program: Command): void { headers: { "content-type": "application/json", "x-api-key": apiKey.apiKey, - "x-project-id": projectId.projectId, + ...(projectId ? { "x-project-id": projectId.projectId } : {}), }, body: JSON.stringify({ model: MODEL, diff --git a/src/commands/summarize.ts b/src/commands/summarize.ts index 50ea64d..93f85b9 100644 --- a/src/commands/summarize.ts +++ b/src/commands/summarize.ts @@ -16,9 +16,9 @@ export function registerSummarizeCommand(program: Command): void { const apiKey = getApiKey(); const projectId = getProjectId(); - if (!apiKey || !projectId) { + if (!apiKey) { console.error( - "You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.", + "You're not fully signed in yet. Run 'zerogpu login' to set your API key.", ); process.exit(1); } @@ -30,7 +30,7 @@ export function registerSummarizeCommand(program: Command): void { headers: { "content-type": "application/json", "x-api-key": apiKey.apiKey, - "x-project-id": projectId.projectId, + ...(projectId ? { "x-project-id": projectId.projectId } : {}), }, body: JSON.stringify({ model: MODEL,