@an-sdk/cli 0.0.8 → 0.0.9

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/AGENTS.md ADDED
@@ -0,0 +1,19 @@
1
+ # @an-sdk/cli
2
+
3
+ CLI tool for deploying AI agents to the AN platform. Two commands: `an login` and `an deploy`.
4
+
5
+ ## Docs
6
+
7
+ Full documentation: `./docs/` directory (8 guides covering the entire AN platform).
8
+
9
+ ## Source
10
+
11
+ Source code: `./src/` directory.
12
+
13
+ ## Key Entry Points
14
+
15
+ - `src/index.ts` — Command router (dispatches to login/deploy)
16
+ - `src/login.ts` — API key auth flow, saves to `~/.an/credentials`
17
+ - `src/deploy.ts` — Bundles agent code with esbuild, deploys to AN platform
18
+ - `src/config.ts` — Reads/writes credentials and project config
19
+ - `src/bundler.ts` — esbuild wrapper (externalizes `@an-sdk/agent`)
@@ -0,0 +1,61 @@
1
+ # AN Platform Overview
2
+
3
+ AN is a platform for building, deploying, and embedding AI agents. You define an agent in TypeScript, deploy it with one command, and embed a chat UI in any React app.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ Your Code (agent.ts)
9
+ |
10
+ v
11
+ an deploy (CLI)
12
+ |
13
+ v
14
+ AN Platform
15
+ |
16
+ +---> E2B Sandbox (isolated Node.js environment)
17
+ | |
18
+ | +---> Claude Agent SDK / Codex (executes your agent)
19
+ | |
20
+ | +---> Your custom tools run here
21
+ |
22
+ +---> AN Relay (relay.an.dev)
23
+ |
24
+ +---> SSE streaming to clients
25
+ |
26
+ +---> Token exchange (API key -> short-lived JWT)
27
+ ```
28
+
29
+ ## Packages
30
+
31
+ | Package | Purpose |
32
+ |---------|---------|
33
+ | `@an-sdk/agent` | Define agents and tools with type inference |
34
+ | `@an-sdk/cli` | Deploy agents from your terminal |
35
+ | `@an-sdk/react` | Chat UI components (theming, tool renderers) |
36
+ | `@an-sdk/nextjs` | Next.js server-side token handler |
37
+ | `@an-sdk/node` | Server-side SDK (sandboxes, threads, tokens) |
38
+
39
+ ## Key Concepts
40
+
41
+ - **Agent**: A TypeScript config defining model, system prompt, tools, and hooks. Runs in a cloud sandbox.
42
+ - **Tool**: A function your agent can call, with a Zod schema for input validation and full type inference.
43
+ - **Sandbox**: An isolated E2B cloud environment where your agent executes. Has Node.js, git, and system tools.
44
+ - **Thread**: A conversation within a sandbox. One sandbox can have multiple threads.
45
+ - **Relay**: The streaming gateway at `relay.an.dev`. Handles auth, routing, and SSE streaming.
46
+
47
+ ## Runtimes
48
+
49
+ AN supports two runtimes:
50
+
51
+ - **`claude-code`** (default) — Uses the Claude Agent SDK. Full tool use, file editing, bash execution.
52
+ - **`codex`** — Uses OpenAI Codex via ACP provider.
53
+
54
+ ## Auth Model
55
+
56
+ Two layers of authentication:
57
+
58
+ 1. **Client -> Relay**: API key (`an_sk_...`) or short-lived JWT (via token exchange)
59
+ 2. **Sandbox -> AI Provider**: Handled internally by the platform (Claude Proxy)
60
+
61
+ For web apps, use `@an-sdk/nextjs` to exchange your API key for a short-lived JWT on the server, so the key never reaches the browser.
@@ -0,0 +1,110 @@
1
+ # Getting Started
2
+
3
+ ## 1. Get an API Key
4
+
5
+ Sign up at [an.dev](https://an.dev) and get your API key from [an.dev/agents/dashboard/api](https://an.dev/agents/dashboard/api).
6
+
7
+ ## 2. Install
8
+
9
+ ```bash
10
+ npm install @an-sdk/agent zod
11
+ ```
12
+
13
+ ## 3. Define Your Agent
14
+
15
+ Create `src/agent.ts`:
16
+
17
+ ```ts
18
+ import { agent, tool } from "@an-sdk/agent"
19
+ import { z } from "zod"
20
+
21
+ export default agent({
22
+ model: "claude-sonnet-4-6",
23
+ systemPrompt: "You are a helpful coding assistant.",
24
+ tools: {
25
+ greet: tool({
26
+ description: "Greet a user by name",
27
+ inputSchema: z.object({ name: z.string() }),
28
+ execute: async ({ name }) => ({
29
+ content: [{ type: "text", text: `Hello, ${name}!` }],
30
+ }),
31
+ }),
32
+ },
33
+ })
34
+ ```
35
+
36
+ ## 4. Login & Deploy
37
+
38
+ ```bash
39
+ npx @an-sdk/cli login
40
+ # Enter your API key: an_sk_...
41
+
42
+ npx @an-sdk/cli deploy
43
+ # Bundling src/agent.ts...
44
+ # Deploying my-agent...
45
+ # https://api.an.dev/v1/chat/my-agent
46
+ ```
47
+
48
+ Your agent is live.
49
+
50
+ ## 5. Embed in a React App
51
+
52
+ ```bash
53
+ npm install @an-sdk/nextjs @an-sdk/react ai @ai-sdk/react
54
+ ```
55
+
56
+ Create a token route (keeps your API key on the server):
57
+
58
+ ```ts
59
+ // app/api/an/token/route.ts
60
+ import { createAnTokenHandler } from "@an-sdk/nextjs/server"
61
+
62
+ export const POST = createAnTokenHandler({
63
+ apiKey: process.env.AN_API_KEY!,
64
+ })
65
+ ```
66
+
67
+ Add the chat UI:
68
+
69
+ ```tsx
70
+ // app/page.tsx
71
+ "use client"
72
+
73
+ import { useChat } from "@ai-sdk/react"
74
+ import { AnAgentChat, createAnChat } from "@an-sdk/nextjs"
75
+ import "@an-sdk/react/styles.css"
76
+ import { useMemo } from "react"
77
+
78
+ export default function Chat() {
79
+ const chat = useMemo(
80
+ () => createAnChat({
81
+ agent: "your-agent-slug",
82
+ tokenUrl: "/api/an/token",
83
+ }),
84
+ [],
85
+ )
86
+
87
+ const { messages, sendMessage, status, stop, error } = useChat({ chat })
88
+
89
+ return (
90
+ <AnAgentChat
91
+ messages={messages}
92
+ onSend={(msg) =>
93
+ sendMessage({ parts: [{ type: "text", text: msg.content }] })
94
+ }
95
+ status={status}
96
+ onStop={stop}
97
+ error={error}
98
+ />
99
+ )
100
+ }
101
+ ```
102
+
103
+ ## Next Steps
104
+
105
+ - [Defining Agents](./03-defining-agents.md) — Models, tools, hooks, permissions
106
+ - [React UI](./04-react-ui.md) — Theming, slots, tool renderers
107
+ - [Next.js Integration](./05-nextjs.md) — Server-side token handler
108
+ - [Node SDK](./06-node-sdk.md) — Sandboxes, threads, tokens from server code
109
+ - [CLI Reference](./07-cli.md) — All CLI commands
110
+ - [Custom Tools](./08-custom-tools.md) — Build custom tool renderers
@@ -0,0 +1,159 @@
1
+ # Defining Agents
2
+
3
+ Agents are defined with the `agent()` and `tool()` functions from `@an-sdk/agent`. These are config-only — they return exactly what you pass in, with type inference added. The actual execution happens in the AN runtime (E2B sandbox).
4
+
5
+ ## `agent(config)`
6
+
7
+ ```ts
8
+ import { agent } from "@an-sdk/agent"
9
+
10
+ export default agent({
11
+ // Model (default: "claude-sonnet-4-6")
12
+ model: "claude-sonnet-4-6",
13
+
14
+ // System prompt
15
+ systemPrompt: "You are a PR reviewer...",
16
+
17
+ // Custom tools (see below)
18
+ tools: { /* ... */ },
19
+
20
+ // Runtime: "claude-code" (default) or "codex"
21
+ runtime: "claude-code",
22
+
23
+ // Permission mode: "default", "acceptEdits", or "bypassPermissions"
24
+ permissionMode: "default",
25
+
26
+ // Max conversation turns (default: 50)
27
+ maxTurns: 50,
28
+
29
+ // Max budget in USD
30
+ maxBudgetUsd: 5,
31
+
32
+ // Lifecycle hooks (see below)
33
+ onStart: async () => {},
34
+ onToolCall: async ({ toolName, input }) => {},
35
+ onToolResult: async ({ toolName, input, output }) => {},
36
+ onStepFinish: async ({ step }) => {},
37
+ onFinish: async ({ result }) => {},
38
+ onError: async ({ error }) => {},
39
+ })
40
+ ```
41
+
42
+ ### Defaults
43
+
44
+ | Field | Default |
45
+ |-------|---------|
46
+ | `model` | `"claude-sonnet-4-6"` |
47
+ | `runtime` | `"claude-code"` |
48
+ | `permissionMode` | `"default"` |
49
+ | `maxTurns` | `50` |
50
+ | `tools` | `{}` |
51
+
52
+ ## `tool(definition)`
53
+
54
+ ```ts
55
+ import { tool } from "@an-sdk/agent"
56
+ import { z } from "zod"
57
+
58
+ const myTool = tool({
59
+ description: "What this tool does",
60
+ inputSchema: z.object({
61
+ path: z.string(),
62
+ verbose: z.boolean().optional(),
63
+ }),
64
+ execute: async (args) => {
65
+ // args is fully typed: { path: string; verbose?: boolean }
66
+ return {
67
+ content: [{ type: "text", text: "result" }],
68
+ }
69
+ },
70
+ })
71
+ ```
72
+
73
+ The `execute` function receives args typed from the Zod schema. Return value must have `content` array with `{ type: "text", text: string }` items.
74
+
75
+ ## Hooks
76
+
77
+ ### `onToolCall`
78
+
79
+ Runs before a tool executes. Return `false` to block it.
80
+
81
+ ```ts
82
+ onToolCall: async ({ toolName, input }) => {
83
+ if (toolName === "Bash" && input.command?.includes("rm -rf")) {
84
+ return false // blocked
85
+ }
86
+ }
87
+ ```
88
+
89
+ ### `onToolResult`
90
+
91
+ Runs after a tool executes with the result.
92
+
93
+ ```ts
94
+ onToolResult: async ({ toolName, input, output }) => {
95
+ console.log(`Tool ${toolName} returned:`, output)
96
+ }
97
+ ```
98
+
99
+ ### `onFinish`
100
+
101
+ Runs when the agent completes successfully.
102
+
103
+ ```ts
104
+ onFinish: async ({ result }) => {
105
+ await fetch("https://my-api.com/webhook", {
106
+ method: "POST",
107
+ body: JSON.stringify({ done: true }),
108
+ })
109
+ }
110
+ ```
111
+
112
+ ### `onError`
113
+
114
+ Runs when the agent encounters an error.
115
+
116
+ ```ts
117
+ onError: async ({ error }) => {
118
+ console.error("Agent failed:", error)
119
+ }
120
+ ```
121
+
122
+ ## Permission Modes
123
+
124
+ | Mode | Behavior |
125
+ |------|----------|
126
+ | `"default"` | Agent asks for confirmation on risky operations |
127
+ | `"acceptEdits"` | Auto-approve file edits, still confirm other actions |
128
+ | `"bypassPermissions"` | Auto-approve everything (use with caution) |
129
+
130
+ ## Type Reference
131
+
132
+ ```ts
133
+ // Agent config (all fields required — defaults filled by agent())
134
+ interface AgentConfig {
135
+ model: string
136
+ systemPrompt: string
137
+ tools: ToolSet
138
+ runtime: "claude-code" | "codex"
139
+ permissionMode: "default" | "acceptEdits" | "bypassPermissions"
140
+ maxTurns: number
141
+ maxBudgetUsd?: number
142
+ onStart?: () => Promise<void>
143
+ onToolCall?: (payload: { toolName: string; input: any }) => Promise<boolean | void>
144
+ onToolResult?: (payload: { toolName: string; input: any; output: any }) => Promise<void>
145
+ onStepFinish?: (payload: { step: any }) => Promise<void>
146
+ onFinish?: (payload: { result: any }) => Promise<void>
147
+ onError?: (payload: { error: Error }) => Promise<void>
148
+ }
149
+
150
+ // Tool definition — generic over Zod schema
151
+ interface ToolDefinition<TInput> {
152
+ description: string
153
+ inputSchema: ZodType<TInput>
154
+ execute: (args: TInput) => Promise<{ content: { type: string; text: string }[] }>
155
+ }
156
+
157
+ // Tool set
158
+ type ToolSet = Record<string, ToolDefinition<any>>
159
+ ```
@@ -0,0 +1,186 @@
1
+ # React UI
2
+
3
+ `@an-sdk/react` provides a full chat UI for AN agents. Built on [Vercel AI SDK v5](https://sdk.vercel.ai) — uses standard `useChat()` from `@ai-sdk/react`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @an-sdk/react ai @ai-sdk/react
9
+ ```
10
+
11
+ ## Basic Usage
12
+
13
+ ```tsx
14
+ "use client"
15
+
16
+ import { useChat } from "@ai-sdk/react"
17
+ import { AnAgentChat, createAnChat } from "@an-sdk/react"
18
+ import "@an-sdk/react/styles.css"
19
+ import { useMemo } from "react"
20
+
21
+ export default function Chat() {
22
+ const chat = useMemo(
23
+ () => createAnChat({
24
+ agent: "your-agent-slug",
25
+ getToken: async () => "your_an_sk_token",
26
+ }),
27
+ [],
28
+ )
29
+
30
+ const { messages, sendMessage, status, stop, error } = useChat({ chat })
31
+
32
+ return (
33
+ <AnAgentChat
34
+ messages={messages}
35
+ onSend={(msg) =>
36
+ sendMessage({ parts: [{ type: "text", text: msg.content }] })
37
+ }
38
+ status={status}
39
+ onStop={stop}
40
+ error={error}
41
+ />
42
+ )
43
+ }
44
+ ```
45
+
46
+ ## `createAnChat(options)`
47
+
48
+ Creates an AI SDK `Chat` instance pointed at the AN Relay.
49
+
50
+ ```ts
51
+ createAnChat({
52
+ agent: string // Agent slug from dashboard
53
+ getToken: () => Promise<string> // Returns an_sk_ key or JWT
54
+ apiUrl?: string // Default: "https://relay.an.dev"
55
+ projectId?: string // Session persistence key
56
+ onFinish?: () => void
57
+ onError?: (error: Error) => void
58
+ })
59
+ ```
60
+
61
+ For Next.js apps, use `tokenUrl` instead of `getToken` — see [Next.js Integration](./05-nextjs.md).
62
+
63
+ ## `<AnAgentChat />` Props
64
+
65
+ | Prop | Type | Description |
66
+ |------|------|-------------|
67
+ | `messages` | `UIMessage[]` | From `useChat()` |
68
+ | `onSend` | `(msg) => void` | Send handler |
69
+ | `status` | `ChatStatus` | `"ready" \| "submitted" \| "streaming" \| "error"` |
70
+ | `onStop` | `() => void` | Stop generation |
71
+ | `error` | `Error` | Error to display |
72
+ | `theme` | `AnTheme` | Theme from playground |
73
+ | `colorMode` | `"light" \| "dark" \| "auto"` | Color mode |
74
+ | `classNames` | `Partial<AnClassNames>` | Per-element CSS overrides |
75
+ | `slots` | `Partial<AnSlots>` | Component swapping |
76
+ | `className` | `string` | Root element class |
77
+ | `style` | `CSSProperties` | Root element style |
78
+
79
+ ## Customization
80
+
81
+ Four levels, from simple to full control:
82
+
83
+ ### 1. Theme tokens
84
+
85
+ Apply a theme JSON from the [AN Playground](https://an.dev/an/playground):
86
+
87
+ ```tsx
88
+ <AnAgentChat theme={playgroundTheme} colorMode="dark" />
89
+ ```
90
+
91
+ ### 2. Class overrides
92
+
93
+ ```tsx
94
+ <AnAgentChat
95
+ classNames={{
96
+ root: "rounded-2xl border",
97
+ messageList: "px-8",
98
+ inputBar: "bg-gray-50",
99
+ userMessage: "bg-blue-100",
100
+ }}
101
+ />
102
+ ```
103
+
104
+ ### 3. Slot components
105
+
106
+ Swap sub-components:
107
+
108
+ ```tsx
109
+ <AnAgentChat
110
+ slots={{
111
+ InputBar: MyCustomInput,
112
+ UserMessage: MyUserBubble,
113
+ ToolRenderer: MyToolRenderer,
114
+ }}
115
+ />
116
+ ```
117
+
118
+ ### 4. Individual component imports
119
+
120
+ ```tsx
121
+ import { MessageList, InputBar, ToolRenderer } from "@an-sdk/react"
122
+ ```
123
+
124
+ ## CSS
125
+
126
+ Import once in your app:
127
+
128
+ ```tsx
129
+ import "@an-sdk/react/styles.css"
130
+ ```
131
+
132
+ No Tailwind peer dependency — CSS is pre-compiled. All elements have stable `an-*` class names:
133
+
134
+ ```css
135
+ .an-root { }
136
+ .an-message-list { }
137
+ .an-user-message { }
138
+ .an-assistant-message { }
139
+ .an-input-bar { }
140
+ .an-send-button { }
141
+ .an-tool-bash { }
142
+ .an-tool-edit { }
143
+ ```
144
+
145
+ ## Theme Type
146
+
147
+ ```ts
148
+ interface AnTheme {
149
+ theme: Record<string, string> // Shared: font, spacing, accent
150
+ light: Record<string, string> // Light mode CSS vars
151
+ dark: Record<string, string> // Dark mode CSS vars
152
+ }
153
+ ```
154
+
155
+ ## `applyTheme(element, theme, colorMode?)`
156
+
157
+ Manually inject CSS variables from a theme JSON onto a DOM element.
158
+
159
+ ## Components
160
+
161
+ | Component | Description |
162
+ |-----------|-------------|
163
+ | `MessageList` | Auto-scrolling message container with grouping |
164
+ | `UserMessage` | User message bubble |
165
+ | `AssistantMessage` | Assistant response with parts routing |
166
+ | `StreamingMarkdown` | Markdown renderer with syntax highlighting |
167
+ | `InputBar` | Text input with send/stop buttons |
168
+ | `MessageActions` | Copy button |
169
+ | `ToolRenderer` | Routes tool parts to the correct component |
170
+
171
+ ## Built-in Tool Renderers
172
+
173
+ | Component | Renders |
174
+ |-----------|---------|
175
+ | `BashTool` | Terminal commands with output |
176
+ | `EditTool` | File edits with diff display |
177
+ | `WriteTool` | File creation |
178
+ | `SearchTool` | Web search results |
179
+ | `TodoTool` | Task checklists |
180
+ | `PlanTool` | Step-by-step plans |
181
+ | `TaskTool` | Sub-agent tasks |
182
+ | `McpTool` | MCP protocol calls |
183
+ | `ThinkingTool` | Reasoning/thinking indicator |
184
+ | `GenericTool` | Fallback for unknown tools |
185
+
186
+ See [Custom Tools](./08-custom-tools.md) for building your own tool renderers.
@@ -0,0 +1,117 @@
1
+ # Next.js Integration
2
+
3
+ `@an-sdk/nextjs` provides a server-side token handler so your `an_sk_` API key never reaches the browser. It also re-exports everything from `@an-sdk/react` for convenience.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @an-sdk/nextjs @an-sdk/react ai @ai-sdk/react
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ ### 1. Set your API key
14
+
15
+ ```env
16
+ # .env.local
17
+ AN_API_KEY=an_sk_your_key_here
18
+ ```
19
+
20
+ Get your API key from [an.dev/agents/dashboard/api](https://an.dev/agents/dashboard/api).
21
+
22
+ ### 2. Create the token route
23
+
24
+ ```ts
25
+ // app/api/an/token/route.ts
26
+ import { createAnTokenHandler } from "@an-sdk/nextjs/server"
27
+
28
+ export const POST = createAnTokenHandler({
29
+ apiKey: process.env.AN_API_KEY!,
30
+ })
31
+ ```
32
+
33
+ ### 3. Use in your page
34
+
35
+ ```tsx
36
+ // app/page.tsx
37
+ "use client"
38
+
39
+ import { useChat } from "@ai-sdk/react"
40
+ import { AnAgentChat, createAnChat } from "@an-sdk/nextjs"
41
+ import "@an-sdk/react/styles.css"
42
+ import { useMemo } from "react"
43
+
44
+ export default function Chat() {
45
+ const chat = useMemo(
46
+ () => createAnChat({
47
+ agent: "your-agent-slug",
48
+ tokenUrl: "/api/an/token",
49
+ }),
50
+ [],
51
+ )
52
+
53
+ const { messages, sendMessage, status, stop, error } = useChat({ chat })
54
+
55
+ return (
56
+ <AnAgentChat
57
+ messages={messages}
58
+ onSend={(msg) =>
59
+ sendMessage({ parts: [{ type: "text", text: msg.content }] })
60
+ }
61
+ status={status}
62
+ onStop={stop}
63
+ error={error}
64
+ />
65
+ )
66
+ }
67
+ ```
68
+
69
+ ## How It Works
70
+
71
+ ```
72
+ Browser Your Next.js Server AN Relay
73
+ | | |
74
+ |-- POST /api/an/token --------->| |
75
+ | |-- POST /v1/tokens -------->|
76
+ | | (with an_sk_ key) |
77
+ | |<-- { token, expiresAt } ---|
78
+ |<-- { token, expiresAt } ------| |
79
+ | |
80
+ |-- POST /v1/chat/:agent ------(with short-lived JWT)------->|
81
+ |<-- streaming response -----(SSE)---------------------------|
82
+ ```
83
+
84
+ The client only receives short-lived JWTs. Your API key stays on the server.
85
+
86
+ ## API
87
+
88
+ ### `createAnTokenHandler(options)`
89
+
90
+ Returns a Next.js `POST` route handler.
91
+
92
+ ```ts
93
+ createAnTokenHandler({
94
+ apiKey: string // Your an_sk_ API key
95
+ relayUrl?: string // Default: "https://relay.an.dev"
96
+ expiresIn?: string // Default: "1h"
97
+ })
98
+ ```
99
+
100
+ ### `exchangeToken(options)`
101
+
102
+ Lower-level function for custom token exchange logic.
103
+
104
+ ```ts
105
+ import { exchangeToken } from "@an-sdk/nextjs/server"
106
+
107
+ const { token, expiresAt } = await exchangeToken({
108
+ apiKey: process.env.AN_API_KEY!,
109
+ relayUrl: "https://relay.an.dev",
110
+ expiresIn: "1h",
111
+ })
112
+ ```
113
+
114
+ ## Entry Points
115
+
116
+ - `@an-sdk/nextjs` — Re-exports everything from `@an-sdk/react` (components, types, `createAnChat`)
117
+ - `@an-sdk/nextjs/server` — Server-only: `createAnTokenHandler`, `exchangeToken`
@@ -0,0 +1,125 @@
1
+ # Node SDK
2
+
3
+ `@an-sdk/node` is a server-side client for the AN API. Use it to manage sandboxes, threads, and tokens programmatically.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @an-sdk/node
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```ts
14
+ import { AnClient } from "@an-sdk/node"
15
+
16
+ const an = new AnClient({
17
+ apiKey: process.env.AN_API_KEY!, // an_sk_...
18
+ })
19
+
20
+ // Create a sandbox for your agent
21
+ const sandbox = await an.sandboxes.create({ agent: "my-agent" })
22
+
23
+ // Create a thread
24
+ const thread = await an.threads.create({
25
+ sandboxId: sandbox.sandboxId,
26
+ name: "Review PR #42",
27
+ })
28
+
29
+ // Generate a short-lived token for browser clients
30
+ const { token, expiresAt } = await an.tokens.create({
31
+ agent: "my-agent",
32
+ expiresIn: "1h",
33
+ })
34
+ ```
35
+
36
+ ## `AnClient`
37
+
38
+ ```ts
39
+ new AnClient({
40
+ apiKey: string // Your an_sk_ API key
41
+ baseUrl?: string // Default: "https://relay.an.dev"
42
+ })
43
+ ```
44
+
45
+ ## Resources
46
+
47
+ ### `client.sandboxes`
48
+
49
+ | Method | Description |
50
+ |--------|-------------|
51
+ | `create({ agent })` | Create a new sandbox for an agent |
52
+ | `get(sandboxId)` | Get sandbox details (status, threads, agent info) |
53
+ | `delete(sandboxId)` | Delete a sandbox |
54
+
55
+ ### `client.threads`
56
+
57
+ | Method | Description |
58
+ |--------|-------------|
59
+ | `list({ sandboxId })` | List all threads in a sandbox |
60
+ | `create({ sandboxId, name? })` | Create a new thread |
61
+ | `get({ sandboxId, threadId })` | Get thread with messages |
62
+ | `delete({ sandboxId, threadId })` | Delete a thread |
63
+
64
+ ### `client.tokens`
65
+
66
+ | Method | Description |
67
+ |--------|-------------|
68
+ | `create({ agent?, userId?, expiresIn? })` | Create a short-lived JWT |
69
+
70
+ Default `expiresIn` is `"1h"`.
71
+
72
+ ## Types
73
+
74
+ ```ts
75
+ interface Sandbox {
76
+ id: string
77
+ sandboxId: string
78
+ status: string
79
+ createdAt: string
80
+ }
81
+
82
+ interface SandboxDetail {
83
+ id: string
84
+ sandboxId: string
85
+ status: string
86
+ error?: string | null
87
+ agent: { slug: string; name: string }
88
+ threads: ThreadSummary[]
89
+ createdAt: string
90
+ updatedAt: string
91
+ }
92
+
93
+ interface ThreadSummary {
94
+ id: string
95
+ name?: string | null
96
+ status: string
97
+ createdAt: string
98
+ }
99
+
100
+ interface Thread {
101
+ id: string
102
+ name?: string | null
103
+ status: string
104
+ messages?: unknown
105
+ createdAt: string
106
+ updatedAt: string
107
+ }
108
+
109
+ interface Token {
110
+ token: string
111
+ expiresAt: string
112
+ }
113
+ ```
114
+
115
+ ## Error Handling
116
+
117
+ All methods throw on non-2xx responses. The error message comes from the API response body when available.
118
+
119
+ ```ts
120
+ try {
121
+ const sandbox = await an.sandboxes.get("nonexistent")
122
+ } catch (err) {
123
+ console.error(err.message) // "Sandbox not found" or similar
124
+ }
125
+ ```
package/docs/07-cli.md ADDED
@@ -0,0 +1,88 @@
1
+ # CLI Reference
2
+
3
+ `@an-sdk/cli` provides the `an` command for deploying agents.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @an-sdk/cli
9
+ # or use npx
10
+ npx @an-sdk/cli <command>
11
+ ```
12
+
13
+ ## Commands
14
+
15
+ ### `an login`
16
+
17
+ Authenticate with the AN platform.
18
+
19
+ ```bash
20
+ npx @an-sdk/cli login
21
+ # Enter your API key: an_sk_...
22
+ # Authenticated as John (team: my-team)
23
+ ```
24
+
25
+ Your key is saved to `~/.an/credentials`.
26
+
27
+ ### `an deploy`
28
+
29
+ Bundle and deploy your agent.
30
+
31
+ ```bash
32
+ npx @an-sdk/cli deploy
33
+ # Bundling src/agent.ts...
34
+ # Bundled (12.3kb)
35
+ # Deploying my-agent...
36
+ # https://api.an.dev/v1/chat/my-agent
37
+ ```
38
+
39
+ The CLI:
40
+ 1. Finds your entry point (see detection order below)
41
+ 2. Bundles your code + dependencies with esbuild
42
+ 3. Deploys to a secure cloud sandbox
43
+ 4. Returns your agent's URL
44
+
45
+ ## Entry Point Detection
46
+
47
+ The CLI looks for your agent file in this order:
48
+
49
+ 1. `src/agent.ts`
50
+ 2. `src/index.ts`
51
+ 3. `agent.ts`
52
+ 4. `index.ts`
53
+
54
+ Your entry file must `export default agent(...)`.
55
+
56
+ ## Project Linking
57
+
58
+ After first deploy, the CLI saves `.an/project.json` in your project directory:
59
+
60
+ ```json
61
+ {
62
+ "agentId": "abc123",
63
+ "slug": "my-agent"
64
+ }
65
+ ```
66
+
67
+ Subsequent deploys update the existing agent.
68
+
69
+ ## Bundling
70
+
71
+ The CLI uses esbuild to bundle your agent code:
72
+
73
+ - Target: Node 22, ESM
74
+ - `@an-sdk/agent` is externalized (provided by the sandbox runtime)
75
+ - All other dependencies are bundled into a single file
76
+
77
+ ## Configuration Files
78
+
79
+ | File | Location | Purpose |
80
+ |------|----------|---------|
81
+ | `~/.an/credentials` | Global | API key (`{ "apiKey": "an_sk_..." }`) |
82
+ | `.an/project.json` | Per-project | Agent ID and slug for redeployment |
83
+
84
+ ## Environment Variables
85
+
86
+ | Variable | Default | Description |
87
+ |----------|---------|-------------|
88
+ | `AN_API_URL` | `https://an.dev/api/v1` | Override API endpoint |
@@ -0,0 +1,88 @@
1
+ # Custom Tool Renderers
2
+
3
+ The `@an-sdk/react` chat UI automatically renders tool calls from your agent. It includes built-in renderers for all standard Claude tools and supports custom renderers for your own tools.
4
+
5
+ ## Built-in Tool Renderers
6
+
7
+ These are rendered automatically when your agent uses standard tools:
8
+
9
+ | Tool | Renderer | Display |
10
+ |------|----------|---------|
11
+ | `Bash` | `BashTool` | Terminal card with command and output |
12
+ | `Edit` | `EditTool` | Diff card with file path and changes |
13
+ | `Write` | `WriteTool` | File creation card |
14
+ | `WebSearch` | `SearchTool` | Search results list |
15
+ | `TodoWrite` | `TodoTool` | Task checklist with progress |
16
+ | `EnterPlanMode` | `PlanTool` | Step list with progress bar |
17
+ | `Task` | `TaskTool` | Sub-agent task with nested tools |
18
+ | `mcp__*` | `McpTool` | MCP tool call with params and output |
19
+ | `thinking` | `ThinkingTool` | Collapsible reasoning block |
20
+ | Other | `GenericTool` | Fallback JSON display |
21
+
22
+ ## Custom Tool Renderers via Slots
23
+
24
+ To render your custom tools differently, use the `slots.ToolRenderer` prop:
25
+
26
+ ```tsx
27
+ import { AnAgentChat, ToolRenderer } from "@an-sdk/react"
28
+ import type { ToolPart } from "@an-sdk/react"
29
+
30
+ function MyToolRenderer(props: { part: ToolPart; status: string }) {
31
+ const { part, status } = props
32
+
33
+ // Handle your custom tool
34
+ if (part.toolInvocation.toolName === "weather") {
35
+ const args = part.toolInvocation.args
36
+ const result = part.toolInvocation.state === "result"
37
+ ? part.toolInvocation.result
38
+ : null
39
+
40
+ return (
41
+ <div className="weather-card">
42
+ <h3>Weather for {args.city}</h3>
43
+ {result ? <p>{result.content[0].text}</p> : <p>Loading...</p>}
44
+ </div>
45
+ )
46
+ }
47
+
48
+ // Fall back to default renderer for standard tools
49
+ return <ToolRenderer part={part} status={status} />
50
+ }
51
+
52
+ <AnAgentChat
53
+ slots={{ ToolRenderer: MyToolRenderer }}
54
+ // ... other props
55
+ />
56
+ ```
57
+
58
+ ## MCP Tool Naming
59
+
60
+ MCP (Model Context Protocol) tools follow the naming pattern `mcp__<server>__<tool>`. The built-in `McpTool` renderer parses this automatically and shows the server name and tool name.
61
+
62
+ ## Tool States
63
+
64
+ Each tool invocation has a state:
65
+
66
+ | State | Meaning |
67
+ |-------|---------|
68
+ | `"call"` | Tool has been called, waiting for execution |
69
+ | `"result"` | Tool has returned a result |
70
+
71
+ During streaming, tools may be in `"call"` state before transitioning to `"result"`.
72
+
73
+ ## CSS Classes
74
+
75
+ All tool renderers have stable CSS class names for custom styling:
76
+
77
+ ```css
78
+ .an-tool-bash { }
79
+ .an-tool-edit { }
80
+ .an-tool-write { }
81
+ .an-tool-search { }
82
+ .an-tool-todo { }
83
+ .an-tool-plan { }
84
+ .an-tool-task { }
85
+ .an-tool-mcp { }
86
+ .an-tool-thinking { }
87
+ .an-tool-generic { }
88
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@an-sdk/cli",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "AN CLI — deploy AI agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,9 +8,18 @@
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
+ "docs/**/*",
12
+ "src",
13
+ "!src/**/*.test.ts",
14
+ "AGENTS.md",
11
15
  "README.md"
12
16
  ],
17
+ "directories": {
18
+ "doc": "./docs"
19
+ },
13
20
  "scripts": {
21
+ "prepack": "cp -r ../docs ./docs",
22
+ "postpack": "rm -rf ./docs",
14
23
  "build": "tsup",
15
24
  "dev": "tsx src/index.ts"
16
25
  },
package/src/bundler.ts ADDED
@@ -0,0 +1,63 @@
1
+ import esbuild from "esbuild"
2
+
3
+ export type AgentEntryPoint = { slug: string; entryPoint: string }
4
+
5
+ export async function findAgentEntryPoints(): Promise<AgentEntryPoint[]> {
6
+ const { existsSync, readdirSync, statSync } = await import("fs")
7
+ const { join, basename, extname } = await import("path")
8
+
9
+ if (!existsSync("agents") || !statSync("agents").isDirectory()) {
10
+ throw new Error("No agents/ directory found. See https://an.dev/docs to get started.")
11
+ }
12
+
13
+ const entries: AgentEntryPoint[] = []
14
+ const items = readdirSync("agents")
15
+
16
+ for (const item of items) {
17
+ const fullPath = join("agents", item)
18
+ const stat = statSync(fullPath)
19
+
20
+ if (stat.isDirectory()) {
21
+ for (const indexFile of ["index.ts", "index.js"]) {
22
+ const indexPath = join(fullPath, indexFile)
23
+ if (existsSync(indexPath)) {
24
+ entries.push({ slug: item, entryPoint: indexPath })
25
+ break
26
+ }
27
+ }
28
+ } else if (stat.isFile()) {
29
+ const ext = extname(item)
30
+ if (ext === ".ts" || ext === ".js") {
31
+ entries.push({ slug: basename(item, ext), entryPoint: fullPath })
32
+ }
33
+ }
34
+ }
35
+
36
+ if (entries.length === 0) {
37
+ throw new Error("No agents found in agents/ directory. See https://an.dev/docs to get started.")
38
+ }
39
+
40
+ return entries
41
+ }
42
+
43
+ export async function bundleAgent(entryPoint: string): Promise<Buffer> {
44
+ const result = await esbuild.build({
45
+ entryPoints: [entryPoint],
46
+ bundle: true,
47
+ platform: "node",
48
+ target: "node22",
49
+ format: "esm",
50
+ write: false,
51
+ external: ["@an-sdk/agent"],
52
+ minify: true,
53
+ sourcemap: false,
54
+ })
55
+
56
+ if (result.errors.length > 0) {
57
+ throw new Error(
58
+ `Bundle failed:\n${result.errors.map((e) => e.text).join("\n")}`,
59
+ )
60
+ }
61
+
62
+ return Buffer.from(result.outputFiles[0].contents)
63
+ }
package/src/config.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from "fs"
2
+ import { join } from "path"
3
+ import { homedir } from "os"
4
+
5
+ const AN_DIR = join(homedir(), ".an")
6
+ const CREDENTIALS_PATH = join(AN_DIR, "credentials")
7
+ const PROJECT_PATH = join(process.cwd(), ".an", "project.json")
8
+
9
+ // --- Credentials (global, ~/.an/credentials) ---
10
+
11
+ export function getApiKey(): string | null {
12
+ // Env var takes priority (CI mode)
13
+ if (process.env.AN_API_KEY) return process.env.AN_API_KEY
14
+ try {
15
+ const data = JSON.parse(readFileSync(CREDENTIALS_PATH, "utf-8"))
16
+ return data.apiKey || null
17
+ } catch {
18
+ return null
19
+ }
20
+ }
21
+
22
+ export function saveApiKey(apiKey: string): void {
23
+ mkdirSync(AN_DIR, { recursive: true })
24
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify({ apiKey }, null, 2))
25
+ }
26
+
27
+ // --- Project linking (local, .an/project.json) ---
28
+
29
+ export type ProjectConfig = { projectId: string; projectSlug: string }
30
+
31
+ export function getProject(): ProjectConfig | null {
32
+ try {
33
+ const data = JSON.parse(readFileSync(PROJECT_PATH, "utf-8"))
34
+ // Backward compat: old format had { agentId, slug }
35
+ if (data.projectId && data.projectSlug) return data
36
+ if (data.slug) return { projectId: data.agentId ?? "", projectSlug: data.slug }
37
+ return null
38
+ } catch {
39
+ return null
40
+ }
41
+ }
42
+
43
+ export function saveProject(data: ProjectConfig): void {
44
+ mkdirSync(join(process.cwd(), ".an"), { recursive: true })
45
+ writeFileSync(PROJECT_PATH, JSON.stringify(data, null, 2))
46
+ }
package/src/deploy.ts ADDED
@@ -0,0 +1,177 @@
1
+ import { getApiKey, getProject, saveProject } from "./config.js"
2
+ import { findAgentEntryPoints, bundleAgent } from "./bundler.js"
3
+ import { basename } from "path"
4
+ import * as p from "@clack/prompts"
5
+
6
+ function slugify(name: string): string {
7
+ return name
8
+ .toLowerCase()
9
+ .replace(/[^a-z0-9]+/g, "-")
10
+ .replace(/^-|-$/g, "")
11
+ }
12
+
13
+ const API_BASE = process.env.AN_API_URL || "https://an.dev/api/v1"
14
+ const AN_BASE = process.env.AN_URL || "https://an.dev"
15
+
16
+ type ProjectInfo = { id: string; slug: string; name: string }
17
+
18
+ async function fetchProjects(apiKey: string): Promise<ProjectInfo[]> {
19
+ const res = await fetch(`${API_BASE}/projects`, {
20
+ headers: { Authorization: `Bearer ${apiKey}` },
21
+ })
22
+ if (!res.ok) return []
23
+ const data = await res.json()
24
+ return data.projects ?? []
25
+ }
26
+
27
+ async function linkProject(apiKey: string): Promise<{ projectId: string; projectSlug: string }> {
28
+ const projects = await fetchProjects(apiKey)
29
+
30
+ if (projects.length === 0) {
31
+ // No existing projects — just ask for a name
32
+ const name = await p.text({
33
+ message: "Project name",
34
+ defaultValue: basename(process.cwd()),
35
+ placeholder: basename(process.cwd()),
36
+ })
37
+ if (p.isCancel(name)) {
38
+ p.cancel("Deploy cancelled.")
39
+ process.exit(0)
40
+ }
41
+ return { projectId: "", projectSlug: slugify(name) }
42
+ }
43
+
44
+ // Has existing projects — let them pick or create new
45
+ const options = [
46
+ { value: "__new__", label: "Create a new project" },
47
+ ...projects.map((proj) => ({ value: proj.slug, label: proj.name || proj.slug })),
48
+ ]
49
+
50
+ const choice = await p.select({
51
+ message: "Link to a project",
52
+ options,
53
+ })
54
+ if (p.isCancel(choice)) {
55
+ p.cancel("Deploy cancelled.")
56
+ process.exit(0)
57
+ }
58
+
59
+ if (choice === "__new__") {
60
+ const name = await p.text({
61
+ message: "Project name",
62
+ defaultValue: basename(process.cwd()),
63
+ placeholder: basename(process.cwd()),
64
+ })
65
+ if (p.isCancel(name)) {
66
+ p.cancel("Deploy cancelled.")
67
+ process.exit(0)
68
+ }
69
+ return { projectId: "", projectSlug: slugify(name) }
70
+ }
71
+
72
+ // Linked to existing project
73
+ const selected = projects.find((proj) => proj.slug === choice)!
74
+ return { projectId: selected.id, projectSlug: selected.slug }
75
+ }
76
+
77
+ export async function deploy() {
78
+ p.intro("an deploy")
79
+
80
+ // 1. Check auth
81
+ const apiKey = getApiKey()
82
+ if (!apiKey) {
83
+ p.log.error("Not logged in. Run `an login` first, or set AN_API_KEY env var.")
84
+ process.exit(1)
85
+ }
86
+
87
+ // 2. Find agent entry points
88
+ let agents
89
+ try {
90
+ agents = await findAgentEntryPoints()
91
+ } catch (err: any) {
92
+ p.log.error(err.message)
93
+ process.exit(1)
94
+ }
95
+ p.log.info(`Found ${agents.length} agent${agents.length > 1 ? "s" : ""}`)
96
+
97
+ // 3. Determine project
98
+ let project = getProject()
99
+ let projectSlug: string
100
+ let projectId: string
101
+
102
+ if (project) {
103
+ projectSlug = project.projectSlug
104
+ projectId = project.projectId
105
+ } else if (process.stdin.isTTY) {
106
+ // Interactive linking
107
+ const linked = await linkProject(apiKey)
108
+ projectSlug = linked.projectSlug
109
+ projectId = linked.projectId
110
+ } else {
111
+ // Non-interactive: auto-create from cwd name
112
+ projectSlug = slugify(basename(process.cwd()))
113
+ projectId = ""
114
+ }
115
+
116
+ // 4. Deploy each agent
117
+ const deployed: { slug: string }[] = []
118
+
119
+ for (const agent of agents) {
120
+ const s = p.spinner()
121
+ s.start(`Bundling ${agent.slug}...`)
122
+ const bundle = await bundleAgent(agent.entryPoint)
123
+ s.stop(`Bundled ${agent.slug} (${(bundle.length / 1024).toFixed(1)}kb)`)
124
+
125
+ const s2 = p.spinner()
126
+ s2.start(`Deploying ${agent.slug}...`)
127
+ const res = await fetch(`${API_BASE}/agents/deploy`, {
128
+ method: "POST",
129
+ headers: {
130
+ Authorization: `Bearer ${apiKey}`,
131
+ "Content-Type": "application/json",
132
+ },
133
+ body: JSON.stringify({
134
+ projectSlug,
135
+ slug: agent.slug,
136
+ bundle: bundle.toString("base64"),
137
+ }),
138
+ })
139
+
140
+ if (!res.ok) {
141
+ const err = await res.json().catch(() => ({}))
142
+ s2.stop(`Failed to deploy ${agent.slug}`)
143
+ p.log.error((err as any).message || "Deploy failed")
144
+
145
+ if (deployed.length > 0) {
146
+ p.log.info(`\nDeployed before failure:`)
147
+ for (const a of deployed) {
148
+ p.log.info(` ${a.slug} → ${AN_BASE}/a/${projectSlug}/${a.slug}`)
149
+ }
150
+ }
151
+ process.exit(1)
152
+ }
153
+
154
+ const result = await res.json()
155
+ projectId = result.projectId
156
+ projectSlug = result.projectSlug
157
+ deployed.push({ slug: agent.slug })
158
+ s2.stop(`${agent.slug} deployed`)
159
+ }
160
+
161
+ // 5. Save project link
162
+ saveProject({ projectId, projectSlug })
163
+
164
+ // 6. Output
165
+ p.log.success(`Deployed ${deployed.length} agent${deployed.length > 1 ? "s" : ""} to ${projectSlug}`)
166
+ console.log()
167
+ for (const agent of deployed) {
168
+ console.log(` ${agent.slug} → ${AN_BASE}/a/${projectSlug}/${agent.slug}`)
169
+ }
170
+ console.log()
171
+ p.log.info("Next steps:")
172
+ console.log(" · Open the link above to test your agent")
173
+ console.log(" · Run `an deploy` again after changes")
174
+ console.log(` · View all deployments: ${AN_BASE}/an/deployments`)
175
+
176
+ p.outro("Done")
177
+ }
package/src/index.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { login } from "./login.js"
2
+ import { deploy } from "./deploy.js"
3
+ import { createRequire } from "module"
4
+
5
+ const require = createRequire(import.meta.url)
6
+ const { version } = require("../package.json")
7
+
8
+ const command = process.argv[2]
9
+ const args = process.argv.slice(3)
10
+ const hasFlag = (flag: string) => args.includes(flag)
11
+
12
+ function showHelp() {
13
+ console.log(`AN CLI v${version} — deploy AI agents\n`)
14
+ console.log("Usage: an <command>\n")
15
+ console.log("Commands:")
16
+ console.log(" an login Authenticate with AN platform")
17
+ console.log(" an deploy Bundle and deploy your agent")
18
+ console.log("\nOptions:")
19
+ console.log(" --help, -h Show help")
20
+ console.log(" --version, -v Show version")
21
+ console.log("\nGet started: an login")
22
+ console.log("Docs: https://an.dev/docs")
23
+ }
24
+
25
+ function showLoginHelp() {
26
+ console.log("Usage: an login\n")
27
+ console.log("Authenticate with the AN platform using an API key.")
28
+ console.log("Get your API key at https://an.dev/api-keys")
29
+ }
30
+
31
+ function showDeployHelp() {
32
+ console.log("Usage: an deploy\n")
33
+ console.log("Bundle and deploy agents from the ./agents/ directory.\n")
34
+ console.log("Agent structure:")
35
+ console.log(" agents/")
36
+ console.log(" my-agent/")
37
+ console.log(" index.ts agent entry point")
38
+ console.log(" another.ts single-file agent")
39
+ console.log("\nDocs: https://an.dev/docs")
40
+ }
41
+
42
+ if (command === "login") {
43
+ if (hasFlag("--help") || hasFlag("-h")) {
44
+ showLoginHelp()
45
+ } else {
46
+ await login()
47
+ }
48
+ } else if (command === "deploy") {
49
+ if (hasFlag("--help") || hasFlag("-h")) {
50
+ showDeployHelp()
51
+ } else {
52
+ await deploy()
53
+ }
54
+ } else if (command === "--version" || command === "-v") {
55
+ console.log(version)
56
+ } else {
57
+ showHelp()
58
+ }
package/src/login.ts ADDED
@@ -0,0 +1,48 @@
1
+ import * as p from "@clack/prompts"
2
+ import { getApiKey, saveApiKey } from "./config.js"
3
+
4
+ const API_BASE = process.env.AN_API_URL || "https://an.dev/api/v1"
5
+
6
+ export async function login() {
7
+ p.intro("an login")
8
+
9
+ const existing = getApiKey()
10
+ if (existing) {
11
+ p.log.info("Already logged in. Continuing will re-authenticate.")
12
+ }
13
+ p.log.info("Get your API key at https://an.dev/api-keys")
14
+
15
+ const apiKey = await p.text({
16
+ message: "Enter your API key",
17
+ validate: (val) => {
18
+ if (!val.trim()) return "API key cannot be empty"
19
+ },
20
+ })
21
+
22
+ if (p.isCancel(apiKey)) {
23
+ p.cancel("Login cancelled.")
24
+ process.exit(0)
25
+ }
26
+
27
+ const s = p.spinner()
28
+ s.start("Verifying API key...")
29
+
30
+ const res = await fetch(`${API_BASE}/me`, {
31
+ headers: { Authorization: `Bearer ${apiKey.trim()}` },
32
+ })
33
+
34
+ if (!res.ok) {
35
+ s.stop("Invalid API key")
36
+ p.log.error("Invalid API key. Get a new one at https://an.dev/api-keys")
37
+ process.exit(1)
38
+ }
39
+
40
+ const { user, team } = await res.json()
41
+ saveApiKey(apiKey.trim())
42
+ s.stop("Verified")
43
+
44
+ p.log.success(`Authenticated as ${user.displayName || user.email} (team: ${team.name})`)
45
+ p.log.info("Key saved to ~/.an/credentials")
46
+
47
+ p.outro("Done")
48
+ }