@an-sdk/cli 0.0.8 → 0.0.10
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 +19 -0
- package/dist/index.js +303 -114
- package/docs/01-overview.md +61 -0
- package/docs/02-getting-started.md +110 -0
- package/docs/03-defining-agents.md +159 -0
- package/docs/04-react-ui.md +186 -0
- package/docs/05-nextjs.md +117 -0
- package/docs/06-node-sdk.md +125 -0
- package/docs/07-cli.md +88 -0
- package/docs/08-custom-tools.md +88 -0
- package/package.json +10 -1
- package/src/bundler.ts +156 -0
- package/src/config.ts +24 -0
- package/src/deploy.ts +95 -0
- package/src/detect.ts +16 -0
- package/src/env.ts +136 -0
- package/src/index.ts +100 -0
- package/src/login.ts +77 -0
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.
|
|
3
|
+
"version": "0.0.10",
|
|
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,156 @@
|
|
|
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
|
+
}
|
|
64
|
+
|
|
65
|
+
async function importBundle(bundle: Buffer): Promise<Record<string, unknown> | null> {
|
|
66
|
+
const { writeFileSync, unlinkSync, mkdirSync } = await import("fs")
|
|
67
|
+
const { join } = await import("path")
|
|
68
|
+
|
|
69
|
+
// Write to .an-tmp/ in cwd so ESM can resolve @an-sdk/agent from node_modules
|
|
70
|
+
const tmpDir = join(process.cwd(), ".an-tmp")
|
|
71
|
+
mkdirSync(tmpDir, { recursive: true })
|
|
72
|
+
const tmpPath = join(tmpDir, `an-bundle-${Date.now()}.mjs`)
|
|
73
|
+
try {
|
|
74
|
+
writeFileSync(tmpPath, bundle)
|
|
75
|
+
const mod = await import(tmpPath)
|
|
76
|
+
const config = mod.default
|
|
77
|
+
if (config?._type === "agent") return config
|
|
78
|
+
return null
|
|
79
|
+
} catch {
|
|
80
|
+
return null
|
|
81
|
+
} finally {
|
|
82
|
+
try { unlinkSync(tmpPath) } catch {}
|
|
83
|
+
try { const { rmdirSync } = await import("fs"); rmdirSync(tmpDir) } catch {}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function extractSandboxConfig(
|
|
88
|
+
bundle: Buffer,
|
|
89
|
+
): Promise<Record<string, unknown> | null> {
|
|
90
|
+
const config = await importBundle(bundle)
|
|
91
|
+
if (!config) return null
|
|
92
|
+
const sandbox = config.sandbox as Record<string, unknown> | undefined
|
|
93
|
+
if (sandbox && (sandbox as any)._type === "sandbox") {
|
|
94
|
+
const { _type, ...sandboxConfig } = sandbox
|
|
95
|
+
return sandboxConfig
|
|
96
|
+
}
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type AgentMetadata = {
|
|
101
|
+
model: string
|
|
102
|
+
systemPrompt?: string | { type: string; preset: string; append?: string }
|
|
103
|
+
permissionMode: string
|
|
104
|
+
maxTurns: number
|
|
105
|
+
maxBudgetUsd?: number
|
|
106
|
+
tools: { name: string; description: string }[]
|
|
107
|
+
hooks: string[]
|
|
108
|
+
sandbox?: { apt?: string[]; setup?: string[]; cwd?: string }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const HOOK_NAMES = ["onStart", "onToolCall", "onToolResult", "onStepFinish", "onFinish", "onError"]
|
|
112
|
+
|
|
113
|
+
export async function extractAgentMetadata(
|
|
114
|
+
bundle: Buffer,
|
|
115
|
+
): Promise<AgentMetadata | null> {
|
|
116
|
+
try {
|
|
117
|
+
const config = await importBundle(bundle)
|
|
118
|
+
if (!config) return null
|
|
119
|
+
|
|
120
|
+
const tools: { name: string; description: string }[] = []
|
|
121
|
+
if (config.tools && typeof config.tools === "object") {
|
|
122
|
+
for (const [name, def] of Object.entries(config.tools as Record<string, any>)) {
|
|
123
|
+
tools.push({ name, description: def?.description ?? "" })
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const hooks = HOOK_NAMES.filter((h) => typeof (config as any)[h] === "function")
|
|
128
|
+
|
|
129
|
+
const metadata: AgentMetadata = {
|
|
130
|
+
model: (config.model as string) ?? "unknown",
|
|
131
|
+
permissionMode: (config.permissionMode as string) ?? "default",
|
|
132
|
+
maxTurns: (config.maxTurns as number) ?? 50,
|
|
133
|
+
tools,
|
|
134
|
+
hooks,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (config.systemPrompt !== undefined) {
|
|
138
|
+
metadata.systemPrompt = config.systemPrompt as AgentMetadata["systemPrompt"]
|
|
139
|
+
}
|
|
140
|
+
if (config.maxBudgetUsd !== undefined) {
|
|
141
|
+
metadata.maxBudgetUsd = config.maxBudgetUsd as number
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const sandbox = config.sandbox as Record<string, unknown> | undefined
|
|
145
|
+
if (sandbox && (sandbox as any)._type === "sandbox") {
|
|
146
|
+
metadata.sandbox = {}
|
|
147
|
+
if (sandbox.apt) metadata.sandbox.apt = sandbox.apt as string[]
|
|
148
|
+
if (sandbox.setup) metadata.sandbox.setup = sandbox.setup as string[]
|
|
149
|
+
if (sandbox.cwd) metadata.sandbox.cwd = sandbox.cwd as string
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return metadata
|
|
153
|
+
} catch {
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
|
|
8
|
+
// --- Credentials (global, ~/.an/credentials) ---
|
|
9
|
+
|
|
10
|
+
export function getApiKey(): string | null {
|
|
11
|
+
// Env var takes priority (CI mode)
|
|
12
|
+
if (process.env.AN_API_KEY) return process.env.AN_API_KEY
|
|
13
|
+
try {
|
|
14
|
+
const data = JSON.parse(readFileSync(CREDENTIALS_PATH, "utf-8"))
|
|
15
|
+
return data.apiKey || null
|
|
16
|
+
} catch {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function saveApiKey(apiKey: string): void {
|
|
22
|
+
mkdirSync(AN_DIR, { recursive: true })
|
|
23
|
+
writeFileSync(CREDENTIALS_PATH, JSON.stringify({ apiKey }, null, 2))
|
|
24
|
+
}
|
package/src/deploy.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { getApiKey } from "./config.js"
|
|
2
|
+
import { findAgentEntryPoints, bundleAgent, extractSandboxConfig, extractAgentMetadata } from "./bundler.js"
|
|
3
|
+
import { existsSync } from "fs"
|
|
4
|
+
import { join } from "path"
|
|
5
|
+
import * as p from "@clack/prompts"
|
|
6
|
+
|
|
7
|
+
const API_BASE = process.env.AN_API_URL || "https://an.dev/api/v1"
|
|
8
|
+
const AN_BASE = process.env.AN_URL || "https://an.dev"
|
|
9
|
+
|
|
10
|
+
export async function deploy() {
|
|
11
|
+
p.intro("an deploy")
|
|
12
|
+
|
|
13
|
+
// 1. Check auth
|
|
14
|
+
const apiKey = getApiKey()
|
|
15
|
+
if (!apiKey) {
|
|
16
|
+
p.log.error("Not logged in. Run `an login` first, or set AN_API_KEY env var.")
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Deprecation notice for old project config
|
|
21
|
+
if (existsSync(join(process.cwd(), ".an", "project.json"))) {
|
|
22
|
+
p.log.warn(".an/project.json is no longer used and can be removed.")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 2. Find agent entry points
|
|
26
|
+
let agents
|
|
27
|
+
try {
|
|
28
|
+
agents = await findAgentEntryPoints()
|
|
29
|
+
} catch (err: any) {
|
|
30
|
+
p.log.error(err.message)
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
p.log.info(`Found ${agents.length} agent${agents.length > 1 ? "s" : ""}`)
|
|
34
|
+
|
|
35
|
+
// 3. Deploy each agent
|
|
36
|
+
const deployed: { slug: string }[] = []
|
|
37
|
+
|
|
38
|
+
for (const agent of agents) {
|
|
39
|
+
const s = p.spinner()
|
|
40
|
+
s.start(`Bundling ${agent.slug}...`)
|
|
41
|
+
const bundle = await bundleAgent(agent.entryPoint)
|
|
42
|
+
const sandboxConfig = await extractSandboxConfig(bundle)
|
|
43
|
+
const metadata = await extractAgentMetadata(bundle)
|
|
44
|
+
s.stop(`Bundled ${agent.slug} (${(bundle.length / 1024).toFixed(1)}kb)`)
|
|
45
|
+
|
|
46
|
+
const s2 = p.spinner()
|
|
47
|
+
s2.start(`Deploying ${agent.slug}...`)
|
|
48
|
+
const res = await fetch(`${API_BASE}/agents/deploy`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: {
|
|
51
|
+
Authorization: `Bearer ${apiKey}`,
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
slug: agent.slug,
|
|
56
|
+
bundle: bundle.toString("base64"),
|
|
57
|
+
...(sandboxConfig && { sandboxConfig }),
|
|
58
|
+
...(metadata && { metadata }),
|
|
59
|
+
}),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
const err = await res.json().catch(() => ({}))
|
|
64
|
+
s2.stop(`Failed to deploy ${agent.slug}`)
|
|
65
|
+
p.log.error((err as any).message || "Deploy failed")
|
|
66
|
+
|
|
67
|
+
if (deployed.length > 0) {
|
|
68
|
+
p.log.info(`\nDeployed before failure:`)
|
|
69
|
+
for (const a of deployed) {
|
|
70
|
+
p.log.info(` ${a.slug} → ${AN_BASE}/a/${a.slug}`)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
process.exit(1)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const result = await res.json()
|
|
77
|
+
deployed.push({ slug: agent.slug })
|
|
78
|
+
const versionTag = result.version ? ` (v${result.version})` : ""
|
|
79
|
+
s2.stop(`${agent.slug} deployed${versionTag}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 4. Output
|
|
83
|
+
p.log.success(`Deployed ${deployed.length} agent${deployed.length > 1 ? "s" : ""}`)
|
|
84
|
+
console.log()
|
|
85
|
+
for (const agent of deployed) {
|
|
86
|
+
console.log(` ${agent.slug} → ${AN_BASE}/a/${agent.slug}`)
|
|
87
|
+
}
|
|
88
|
+
console.log()
|
|
89
|
+
p.log.info("Next steps:")
|
|
90
|
+
console.log(" · Open the link above to test your agent")
|
|
91
|
+
console.log(" · Run `an deploy` again after changes")
|
|
92
|
+
console.log(` · View all deployments: ${AN_BASE}/an/deployments`)
|
|
93
|
+
|
|
94
|
+
p.outro("Done")
|
|
95
|
+
}
|
package/src/detect.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Detect AI agents running in pseudo-TTY
|
|
2
|
+
export function isAgent(): boolean {
|
|
3
|
+
return !!(
|
|
4
|
+
process.env.CLAUDE_CODE ||
|
|
5
|
+
process.env.CLAUDECODE ||
|
|
6
|
+
process.env.CURSOR_TRACE_ID ||
|
|
7
|
+
process.env.CURSOR_AGENT ||
|
|
8
|
+
process.env.CODEX_SANDBOX ||
|
|
9
|
+
process.env.GEMINI_CLI ||
|
|
10
|
+
process.env.AI_AGENT
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isInteractive(): boolean {
|
|
15
|
+
return !!process.stdin.isTTY && !isAgent()
|
|
16
|
+
}
|
package/src/env.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { getApiKey } from "./config.js"
|
|
2
|
+
import * as p from "@clack/prompts"
|
|
3
|
+
|
|
4
|
+
const API_BASE = process.env.AN_API_URL || "https://an.dev/api/v1"
|
|
5
|
+
|
|
6
|
+
function maskValue(value: string): string {
|
|
7
|
+
if (value.length <= 4) return "\u2022".repeat(value.length)
|
|
8
|
+
return value.slice(0, 2) + "\u2022".repeat(Math.min(value.length - 4, 12)) + value.slice(-2)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function requireAuth(): string {
|
|
12
|
+
const apiKey = getApiKey()
|
|
13
|
+
if (!apiKey) {
|
|
14
|
+
p.log.error("Not logged in. Run `an login` first, or set AN_API_KEY env var.")
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
return apiKey
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function requireAgentSlug(args: string[]): string {
|
|
21
|
+
const slug = args[0]
|
|
22
|
+
if (!slug || slug.startsWith("-")) {
|
|
23
|
+
p.log.error("Agent slug is required. Usage: an env list <agent-slug>")
|
|
24
|
+
process.exit(1)
|
|
25
|
+
}
|
|
26
|
+
return slug
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function fetchEnvVars(apiKey: string, agentSlug: string): Promise<Record<string, string>> {
|
|
30
|
+
const res = await fetch(`${API_BASE}/agents/${agentSlug}/env`, {
|
|
31
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
32
|
+
})
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
const err = await res.json().catch(() => ({}))
|
|
35
|
+
throw new Error((err as any).message || `Failed to fetch env vars (${res.status})`)
|
|
36
|
+
}
|
|
37
|
+
const data = await res.json()
|
|
38
|
+
return data.envVars ?? {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function putEnvVars(apiKey: string, agentSlug: string, envVars: Record<string, string> | null): Promise<void> {
|
|
42
|
+
const res = await fetch(`${API_BASE}/agents/${agentSlug}/env`, {
|
|
43
|
+
method: "PUT",
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: `Bearer ${apiKey}`,
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({ envVars }),
|
|
49
|
+
})
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
const err = await res.json().catch(() => ({}))
|
|
52
|
+
throw new Error((err as any).message || `Failed to update env vars (${res.status})`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function envList(args: string[]) {
|
|
57
|
+
const apiKey = requireAuth()
|
|
58
|
+
const agentSlug = requireAgentSlug(args)
|
|
59
|
+
const showValues = args.includes("--show-values")
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const vars = await fetchEnvVars(apiKey, agentSlug)
|
|
63
|
+
const keys = Object.keys(vars)
|
|
64
|
+
|
|
65
|
+
if (keys.length === 0) {
|
|
66
|
+
p.log.info("No environment variables set.")
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log()
|
|
71
|
+
for (const key of keys) {
|
|
72
|
+
const display = showValues ? vars[key] : maskValue(vars[key]!)
|
|
73
|
+
console.log(` ${key}=${display}`)
|
|
74
|
+
}
|
|
75
|
+
console.log()
|
|
76
|
+
p.log.info(`${keys.length} variable${keys.length !== 1 ? "s" : ""}`)
|
|
77
|
+
} catch (err: any) {
|
|
78
|
+
p.log.error(err.message)
|
|
79
|
+
process.exit(1)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function envSet(args: string[]) {
|
|
84
|
+
const apiKey = requireAuth()
|
|
85
|
+
const agentSlug = requireAgentSlug(args)
|
|
86
|
+
|
|
87
|
+
const key = args[1]
|
|
88
|
+
const value = args.slice(2).join(" ")
|
|
89
|
+
|
|
90
|
+
if (!key || !value) {
|
|
91
|
+
p.log.error("Usage: an env set <agent-slug> KEY VALUE")
|
|
92
|
+
process.exit(1)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!/^[A-Z0-9_]+$/.test(key)) {
|
|
96
|
+
p.log.error("Invalid key. Use uppercase letters, numbers, and underscores only.")
|
|
97
|
+
process.exit(1)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const vars = await fetchEnvVars(apiKey, agentSlug)
|
|
102
|
+
const existed = key in vars
|
|
103
|
+
vars[key] = value
|
|
104
|
+
await putEnvVars(apiKey, agentSlug, vars)
|
|
105
|
+
p.log.success(existed ? `Updated ${key}` : `Set ${key}`)
|
|
106
|
+
} catch (err: any) {
|
|
107
|
+
p.log.error(err.message)
|
|
108
|
+
process.exit(1)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function envRemove(args: string[]) {
|
|
113
|
+
const apiKey = requireAuth()
|
|
114
|
+
const agentSlug = requireAgentSlug(args)
|
|
115
|
+
|
|
116
|
+
const key = args[1]
|
|
117
|
+
if (!key) {
|
|
118
|
+
p.log.error("Usage: an env remove <agent-slug> KEY")
|
|
119
|
+
process.exit(1)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const vars = await fetchEnvVars(apiKey, agentSlug)
|
|
124
|
+
if (!(key in vars)) {
|
|
125
|
+
p.log.info(`${key} not found`)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
delete vars[key]
|
|
129
|
+
const envVars = Object.keys(vars).length > 0 ? vars : null
|
|
130
|
+
await putEnvVars(apiKey, agentSlug, envVars)
|
|
131
|
+
p.log.success(`Removed ${key}`)
|
|
132
|
+
} catch (err: any) {
|
|
133
|
+
p.log.error(err.message)
|
|
134
|
+
process.exit(1)
|
|
135
|
+
}
|
|
136
|
+
}
|