diff --git a/README.md b/README.md index c16c508..aa8c5f1 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,24 @@ cosmic whoami # Show current user cosmic logout # Clear credentials ``` +### Agent Signup (no prior account) + +For AI agents that need to provision a Cosmic project from scratch on behalf of a human (no prior login required): + +```bash +# Provision a project + bucket tied to a human's email. Cosmic emails them a +# 6-digit claim code. +cosmic agent-signup --email tony@example.com --project "Recipe Blog" --agent-id my-agent + +# When the human pastes the code back to the agent, run: +cosmic agent-verify 123456 + +# Check the current state and tier limits: +cosmic agent-status +``` + +The `agent-signup` command stores the returned `agent_key` and bucket keys in `~/.cosmic/credentials.json` under the `agent` slot, so `agent-verify` and `agent-status` don't need the key on the command line. Unclaimed agent buckets are auto-deleted after 14 days, so plan to verify within that window. + ### Personal Access Token Authenticate with a [Personal Access Token](https://app.cosmicjs.com/account/api-tokens) for scripts, CI/CD, and automation: @@ -501,7 +519,7 @@ cosmic models # List all available models Set your default model: ```bash -cosmic config set defaultModel claude-opus-4-5-20251101 +cosmic config set defaultModel claude-opus-4-7 ``` Or specify per-command: @@ -511,7 +529,7 @@ cosmic ai generate --model=gpt-5 "Your prompt" ``` **Available models:** -- **Claude (Anthropic):** `claude-sonnet-4-6`, `claude-opus-4-5-20251101`, `claude-sonnet-4-5-20250929`, `claude-haiku-4-5-20251001` +- **Claude (Anthropic):** `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-4-6`, `claude-opus-4-5-20251101`, `claude-sonnet-4-5-20250929`, `claude-haiku-4-5-20251001` - **GPT (OpenAI):** `gpt-5`, `gpt-5.2`, `gpt-5-mini`, `gpt-4o` - **Gemini (Google):** `gemini-3-pro-preview` diff --git a/src/api/dashboard/agent.ts b/src/api/dashboard/agent.ts new file mode 100644 index 0000000..0b590cf --- /dev/null +++ b/src/api/dashboard/agent.ts @@ -0,0 +1,141 @@ +/** + * Agent Signup API client. + * + * The existing `client.ts` axios wrapper auto-injects user/session auth + * headers via interceptor, which is wrong for agent signup (no auth) and for + * verify/status (wrong auth shape: agent_key, not user JWT). We use fetch + * directly here to keep the auth surface explicit. + * + * Endpoints (cosmic-backend public.routes.js): + * POST /v3/agents/sign-up + * POST /v3/agents/verify (Authorization: Bearer agk_...) + * GET /v3/agents/status (Authorization: Bearer agk_...) + */ + +import { getApiUrl } from '../../config/store.js'; +import { CLI_VERSION } from '../../version.js'; + +export interface AgentSignupRequest { + human_email: string; + project_name: string; + agent_id: string; + client?: string; + prompt_hint?: string; +} + +export interface AgentSignupResponse { + message: string; + auth_type: 'unclaimed' | 'verified'; + agent_key: string; + project: { id: string; name: string } | null; + bucket: { + slug: string; + read_key?: string; + write_key?: string; + } | null; + claim_url: string; + limits: { + ai_credits_remaining: number; + media_mb_total: number; + objects_max: number; + }; + auto_delete_after_days: number; +} + +export interface AgentVerifyResponse { + message: string; + auth_type: 'verified'; + claim_status: string; + limits: null; +} + +export interface AgentStatusResponse { + auth_type: 'unclaimed' | 'verified'; + claim_status: string; + plan_id: string; + limits: AgentSignupResponse['limits'] | null; + auto_delete_after_days: number | null; + project: { id: string; name: string } | null; + bucket: { slug: string } | null; + agent_id: string | null; + client: string | null; + human_email: string; +} + +async function dapiFetch( + path: string, + init: { method: 'GET' | 'POST'; body?: unknown; agentKey?: string }, +): Promise { + const url = `${getApiUrl()}${path}`; + const headers: Record = { + 'Content-Type': 'application/json', + 'Origin': 'https://app.cosmicjs.com', + 'User-Agent': `CosmicCLI/${CLI_VERSION}`, + 'X-Cosmic-Client': 'cli', + }; + if (init.agentKey) { + headers['Authorization'] = `Bearer ${init.agentKey}`; + } + const res = await fetch(url, { + method: init.method, + headers, + body: init.body !== undefined ? JSON.stringify(init.body) : undefined, + }); + const text = await res.text(); + let parsed: unknown = undefined; + if (text) { + try { + parsed = JSON.parse(text); + } catch { + parsed = text; + } + } + if (!res.ok) { + const message = + typeof parsed === 'object' && parsed !== null && 'message' in parsed + ? String((parsed as { message?: unknown }).message) + : `Agent API request failed (${res.status})`; + const err = new Error(message) as Error & { status: number; code?: string; body?: unknown }; + err.status = res.status; + if ( + typeof parsed === 'object' && + parsed !== null && + 'code' in parsed && + typeof (parsed as { code?: unknown }).code === 'string' + ) { + err.code = (parsed as { code: string }).code; + } + err.body = parsed; + throw err; + } + return parsed as T; +} + +export async function signupAgent( + body: AgentSignupRequest, +): Promise { + return dapiFetch('/agents/sign-up', { + method: 'POST', + body, + }); +} + +export async function verifyAgent( + agentKey: string, + otpCode: string, +): Promise { + return dapiFetch('/agents/verify', { + method: 'POST', + body: { code: otpCode }, + agentKey, + }); +} + +export async function getAgentStatus( + agentKey: string, +): Promise { + return dapiFetch('/agents/status', { + method: 'GET', + agentKey, + }); +} diff --git a/src/api/dashboard/ai.ts b/src/api/dashboard/ai.ts index 95d7af5..c800b84 100644 --- a/src/api/dashboard/ai.ts +++ b/src/api/dashboard/ai.ts @@ -136,7 +136,7 @@ export async function streamingChat(options: StreamingChatOptions): Promise<{ te const { messages, bucketSlug, - model = 'claude-opus-4-5-20251101', + model = 'claude-opus-4-7', maxTokens = 32000, viewMode = 'build-app', selectedObjectTypes = [], @@ -357,7 +357,7 @@ export async function streamingRepositoryUpdate(options: RepositoryUpdateOptions bucketSlug, messages, branch = 'main', - model = 'claude-opus-4-5-20251101', + model = 'claude-opus-4-7', maxTokens = 32000, chatMode = 'agent', onChunk, diff --git a/src/chat/actions.ts b/src/chat/actions.ts index 7478d0a..6edeb3a 100644 --- a/src/chat/actions.ts +++ b/src/chat/actions.ts @@ -275,7 +275,7 @@ export async function executeAction(actionJson: string): Promise { agent_name: action.name, agent_type: action.type || 'content', prompt: action.prompt || 'You are a helpful content writing assistant.', - model: action.model || 'claude-opus-4-5-20251101', + model: action.model || 'claude-opus-4-7', emoji: action.emoji || '🤖', }; diff --git a/src/chat/prompts.ts b/src/chat/prompts.ts index 20c219c..cd8334c 100644 --- a/src/chat/prompts.ts +++ b/src/chat/prompts.ts @@ -278,7 +278,7 @@ You can perform these actions by outputting JSON commands: - agent_name: string (required, 1-100 chars) - agent_type: "content" | "repository" | "computer_use" - prompt: string (required, the system prompt/instructions) -- model: defaults to "claude-opus-4-5-20251101" (don't include unless user specifies different model) +- model: defaults to "claude-opus-4-7" (don't include unless user specifies different model) - emoji: string (always include, e.g. "✍️", "📝", "🤖", "📰", "💡") - object_types: array of object type slugs for context (e.g. ["posts", "authors"]) diff --git a/src/commands/agent.ts b/src/commands/agent.ts new file mode 100644 index 0000000..0dc93b8 --- /dev/null +++ b/src/commands/agent.ts @@ -0,0 +1,228 @@ +/** + * Agent Signup Commands + * + * `cosmic agent-signup` : provision an unclaimed Cosmic project + bucket + * tied to a human's email. + * `cosmic agent-verify` : submit the 6-digit OTP from the claim email. + * `cosmic agent-status` : show current auth_type, plan, claim status. + * + * Credentials persist to ~/.cosmic/credentials.json under the `agent` slot + * so subsequent verify/status calls don't need the agent_key on the CLI. + */ + +import { Command } from 'commander'; +import chalk from 'chalk'; +import { + signupAgent, + verifyAgent, + getAgentStatus, + type AgentSignupResponse, +} from '../api/dashboard/agent.js'; +import { + getCredentials, + setCredentials, +} from '../config/store.js'; +import * as display from '../utils/display.js'; +import * as prompts from '../utils/prompts.js'; +import * as spinner from '../utils/spinner.js'; + +const DEFAULT_AGENT_ID = 'cosmic-cli'; + +interface AgentSignupOptions { + email?: string; + project?: string; + agentId?: string; + client?: string; + promptHint?: string; +} + +async function agentSignupCommand(options: AgentSignupOptions): Promise { + const humanEmail = + options.email || + (await prompts.text({ + message: "Human's email address (required):", + required: true, + })); + + const projectName = + options.project || + (await prompts.text({ + message: 'Project name:', + required: true, + })); + + const agentId = options.agentId || DEFAULT_AGENT_ID; + + try { + spinner.start('Creating agent project...'); + const result: AgentSignupResponse = await signupAgent({ + human_email: humanEmail, + project_name: projectName, + agent_id: agentId, + ...(options.client && { client: options.client }), + ...(options.promptHint && { prompt_hint: options.promptHint }), + }); + spinner.succeed('Agent project created.'); + + setCredentials({ + agent: { + agentKey: result.agent_key, + bucketSlug: result.bucket?.slug, + readKey: result.bucket?.read_key, + writeKey: result.bucket?.write_key, + humanEmail, + projectId: result.project?.id, + projectName: result.project?.name, + authType: result.auth_type, + }, + }); + + display.newline(); + display.header('Agent Project'); + display.keyValue('Project', result.project?.name ?? '(unknown)'); + display.keyValue('Bucket', result.bucket?.slug ?? '(unknown)'); + display.keyValue('Auth type', result.auth_type); + display.keyValue( + 'Limits', + `${result.limits.ai_credits_remaining} AI credits, ${result.limits.objects_max} objects, ${result.limits.media_mb_total} MB media`, + ); + display.keyValue( + 'Auto-delete in', + `${result.auto_delete_after_days} days (unless claimed)`, + ); + + display.newline(); + display.info( + `An email with a 6-digit claim code was sent to ${chalk.cyan(humanEmail)}.`, + ); + display.info( + `When the human gives you the code, run ${chalk.cyan('cosmic agent-verify ')}.`, + ); + display.info( + `Or have the human visit ${chalk.cyan(result.claim_url)} to claim from the dashboard.`, + ); + + display.newline(); + display.dim('Stored agent_key and bucket keys in ~/.cosmic/credentials.json (agent slot).'); + } catch (error) { + spinner.fail('Agent signup failed'); + const err = error as Error & { status?: number; code?: string }; + if (err.code === 'user_already_exists') { + display.error(err.message); + display.info( + `Ask the human to log in at ${chalk.cyan('https://app.cosmicjs.com')} and grant the agent a bucket key instead.`, + ); + } else { + display.error(err.message); + } + process.exit(1); + } +} + +async function agentVerifyCommand(otpArg?: string): Promise { + const creds = getCredentials(); + if (!creds.agent?.agentKey) { + display.error( + `No agent_key found. Run ${chalk.cyan('cosmic agent-signup')} first.`, + ); + process.exit(1); + } + + const otp = + otpArg || + (await prompts.text({ + message: 'Enter the 6-digit OTP from the claim email:', + required: true, + })); + + try { + spinner.start('Verifying agent project...'); + const result = await verifyAgent(creds.agent.agentKey, otp); + spinner.succeed('Verified. Restricted-mode limits lifted.'); + + setCredentials({ + agent: { + ...creds.agent, + authType: result.auth_type, + }, + }); + + display.newline(); + display.info('The bucket is now on standard free-tier limits.'); + display.info( + `Run ${chalk.cyan('cosmic agent-status')} to see current plan and quotas.`, + ); + } catch (error) { + spinner.fail('Verification failed'); + display.error((error as Error).message); + process.exit(1); + } +} + +async function agentStatusCommand(): Promise { + const creds = getCredentials(); + if (!creds.agent?.agentKey) { + display.error( + `No agent_key found. Run ${chalk.cyan('cosmic agent-signup')} first.`, + ); + process.exit(1); + } + + try { + spinner.start('Fetching agent status...'); + const result = await getAgentStatus(creds.agent.agentKey); + spinner.stop(); + + display.header('Agent Status'); + display.keyValue('Human email', result.human_email); + display.keyValue('Agent ID', result.agent_id ?? '(none)'); + display.keyValue('Client', result.client ?? '(none)'); + display.keyValue('Project', result.project?.name ?? '(none)'); + display.keyValue('Bucket', result.bucket?.slug ?? '(none)'); + display.keyValue('Plan', result.plan_id); + display.keyValue('Auth type', result.auth_type); + display.keyValue('Claim status', result.claim_status); + if (result.limits) { + display.keyValue( + 'Limits', + `${result.limits.ai_credits_remaining} AI credits, ${result.limits.objects_max} objects, ${result.limits.media_mb_total} MB media`, + ); + } + if (result.auto_delete_after_days) { + display.keyValue( + 'Auto-delete in', + `${result.auto_delete_after_days} days (unless claimed)`, + ); + } + } catch (error) { + spinner.fail('Failed to fetch agent status'); + display.error((error as Error).message); + process.exit(1); + } +} + +export function createAgentCommands(program: Command): void { + program + .command('agent-signup') + .description( + 'Provision a new Cosmic project + bucket tied to a human email (no prior login needed).', + ) + .option('-e, --email ', "Human's email address") + .option('-p, --project ', 'Project name') + .option('--agent-id ', `Agent platform identifier (default: ${DEFAULT_AGENT_ID})`) + .option('--client ', 'Optional client identifier (e.g. "cursor-1.0.5")') + .option('--prompt-hint ', 'Optional summary of what the human asked for') + .action(agentSignupCommand); + + program + .command('agent-verify [otp]') + .description('Submit the 6-digit OTP from the claim email to lift restricted-mode limits.') + .action(agentVerifyCommand); + + program + .command('agent-status') + .description('Show the current agent project status, plan, and tier limits.') + .action(agentStatusCommand); +} + +export default { createAgentCommands }; diff --git a/src/commands/agents.ts b/src/commands/agents.ts index 4151c02..23854ec 100644 --- a/src/commands/agents.ts +++ b/src/commands/agents.ts @@ -17,8 +17,8 @@ import { captureAuthWithDoneButton, formatCookiesForApi, formatLocalStorageForAp * Default AI models by agent type */ const DEFAULT_MODELS = { - content: 'claude-opus-4-5-20251101', - repository: 'claude-opus-4-5-20251101', + content: 'claude-opus-4-7', + repository: 'claude-opus-4-7', computer_use: 'claude-haiku-4-5-20251001', } as const; diff --git a/src/config/store.ts b/src/config/store.ts index 7c319c6..5f0dcc2 100644 --- a/src/config/store.ts +++ b/src/config/store.ts @@ -18,7 +18,7 @@ const configStore = new Conf({ configName: 'config', defaults: { apiUrl: 'https://dapi.cosmicjs.com/v3', - defaultModel: 'claude-opus-4-5-20251101', + defaultModel: 'claude-opus-4-7', }, }); @@ -261,7 +261,7 @@ export function getCurrentProjectId(): string | undefined { * Get the default AI model */ export function getDefaultModel(): string { - return getConfigValue('defaultModel') || 'claude-opus-4-5-20251101'; + return getConfigValue('defaultModel') || 'claude-opus-4-7'; } /** diff --git a/src/index.ts b/src/index.ts index 3cdc100..2953f22 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,7 @@ if (existsSync(envPath)) { import { Command } from 'commander'; import chalk from 'chalk'; import { createAuthCommands } from './commands/auth.js'; +import { createAgentCommands } from './commands/agent.js'; import { createConfigCommands } from './commands/config.js'; import { createNavigationCommands } from './commands/navigation.js'; import { createObjectsCommands } from './commands/objects.js'; @@ -61,6 +62,7 @@ program // Register all command groups createAuthCommands(program); +createAgentCommands(program); createConfigCommands(program); createNavigationCommands(program); createObjectsCommands(program); diff --git a/src/types.ts b/src/types.ts index 6ad4812..49f8c1b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,6 +24,21 @@ export interface CosmicCredentials { bucketSlug?: string; readKey?: string; writeKey?: string; + // Agent signup auth (stored after `cosmic agent-signup` so subsequent + // `cosmic agent-verify` / `cosmic agent-status` calls can re-use the key + // without the user passing it on the command line). + agent?: CosmicAgentCredentials; +} + +export interface CosmicAgentCredentials { + agentKey: string; + bucketSlug?: string; + readKey?: string; + writeKey?: string; + humanEmail: string; + projectId?: string; + projectName?: string; + authType?: 'unclaimed' | 'verified'; } // User types