@an-sdk/cli 0.0.7 → 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 +19 -0
- package/dist/index.js +52 -10
- 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 +63 -0
- package/src/config.ts +46 -0
- package/src/deploy.ts +177 -0
- package/src/index.ts +58 -0
- package/src/login.ts +48 -0
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
|
+
}
|