@agentvalet/register 0.1.0

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/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # @agentvalet/register
2
+
3
+ Self-register AI agents with [AgentValet](https://agentvalet.ai) — the governance and proxy layer that sits between AI agents and SaaS platforms.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ # One-off use (no install):
9
+ npx @agentvalet/register --name "My Agent" --scope slack:chat:write
10
+
11
+ # Or install globally:
12
+ npm install -g @agentvalet/register
13
+ agentvalet-register --name "My Agent" --scope slack:chat:write
14
+ ```
15
+
16
+ ## Interactive mode
17
+
18
+ Run with no flags for a guided wizard:
19
+
20
+ ```bash
21
+ npx @agentvalet/register
22
+ ```
23
+
24
+ ## Flags
25
+
26
+ | Flag | Default | Description |
27
+ |------|---------|-------------|
28
+ | `--owner <uuid>` | `AGENTVALET_OWNER` env | Owner ID from https://agentvalet.ai/settings |
29
+ | `--name <name>` | prompted | Agent name |
30
+ | `--description <desc>` | — | Agent description |
31
+ | `--scope <platform:scope>` | — | Repeatable, e.g. `--scope slack:chat:write --scope gmail:send` |
32
+ | `--api-url <url>` | `https://api.agentvalet.ai` | AgentValet API base URL |
33
+ | `--output <path>` | `./.env.agentvalet` | Path to write env vars |
34
+ | `--key-path <path>` | `./agentvalet.key` | Path to write private key |
35
+ | `--json` | — | Machine-readable JSON output |
36
+ | `--no-wait` | — | Register only, don't wait for approval |
37
+ | `--timeout <seconds>` | `300` | Approval wait timeout |
38
+
39
+ ## Exit codes
40
+
41
+ | Code | Meaning |
42
+ |------|---------|
43
+ | `0` | Approved |
44
+ | `1` | Denied |
45
+ | `2` | Timeout |
46
+ | `3` | Network or other error |
47
+
48
+ ## Integration examples
49
+
50
+ **Claude Code (`CLAUDE.md`):**
51
+ ```markdown
52
+ On startup, register with AgentValet:
53
+ npx @agentvalet/register --owner {uuid} --name "Claude Code Agent" --scope slack:chat:write
54
+
55
+ Then source ./.env.agentvalet before calling any tool.
56
+ ```
57
+
58
+ **Python agent:**
59
+ ```python
60
+ import subprocess
61
+ subprocess.run([
62
+ "npx", "@agentvalet/register",
63
+ "--name", "MyAgent",
64
+ "--owner", owner_id,
65
+ "--scope", "slack:chat:write",
66
+ ])
67
+ ```
68
+
69
+ **LangChain / CrewAI / bash script:**
70
+
71
+ Shell out to `npx @agentvalet/register` and parse the output env file:
72
+
73
+ ```bash
74
+ npx @agentvalet/register --owner $OWNER_ID --name "BashAgent" --scope slack:chat:write
75
+ source ./.env.agentvalet
76
+ ```
77
+
78
+ ## How it works
79
+
80
+ 1. POSTs to `/v1/register` with your owner ID, agent name, and requested scopes
81
+ 2. Prints the approval URL — the owner approves in the dashboard
82
+ 3. Waits for approval via SSE (with polling fallback)
83
+ 4. On approval, writes the private key to `--key-path` and env vars to `--output`
84
+
85
+ The agent **never holds SaaS credentials** — AgentValet injects them at proxy time.
86
+
87
+ ## Links
88
+
89
+ - Dashboard: https://agentvalet.ai
90
+ - Docs: https://agentvalet.ai/docs
91
+ - GitHub: https://github.com/MCSEdwin/agentvalet
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import chalk2 from "chalk";
6
+
7
+ // src/wizard.ts
8
+ import prompts from "prompts";
9
+ import chalk from "chalk";
10
+ async function runWizard(prefilled) {
11
+ console.log(chalk.bold("\nAgentValet \u2014 Register Agent\n"));
12
+ const questions = [];
13
+ if (!prefilled.ownerId) {
14
+ questions.push({
15
+ type: "text",
16
+ name: "ownerId",
17
+ message: "Owner ID (from https://agentvalet.ai/settings):",
18
+ validate: (v) => v.trim() ? true : "Owner ID is required"
19
+ });
20
+ }
21
+ if (!prefilled.agentName) {
22
+ questions.push({
23
+ type: "text",
24
+ name: "agentName",
25
+ message: "Agent name:",
26
+ initial: "My Agent",
27
+ validate: (v) => v.trim() ? true : "Name is required"
28
+ });
29
+ }
30
+ questions.push(
31
+ {
32
+ type: "text",
33
+ name: "description",
34
+ message: "Description (optional):",
35
+ initial: prefilled.description
36
+ },
37
+ {
38
+ type: "text",
39
+ name: "scopesRaw",
40
+ message: "Scopes (comma-separated, e.g. slack:chat:write,gmail:send):",
41
+ initial: prefilled.scopes.join(",")
42
+ }
43
+ );
44
+ const answers = await prompts(questions, {
45
+ onCancel: () => {
46
+ console.log(chalk.yellow("\nCancelled."));
47
+ process.exit(0);
48
+ }
49
+ });
50
+ const ownerId = answers.ownerId ?? prefilled.ownerId;
51
+ const agentName = answers.agentName ?? prefilled.agentName;
52
+ const description = answers.description ?? prefilled.description;
53
+ const scopesRaw = answers.scopesRaw;
54
+ const scopes = scopesRaw?.split(",").map((s) => s.trim()).filter(Boolean) ?? prefilled.scopes;
55
+ return { ownerId: ownerId.trim(), agentName: agentName.trim(), description, scopes };
56
+ }
57
+
58
+ // src/register.ts
59
+ import { fetch } from "undici";
60
+ async function registerAgent(apiUrl, body) {
61
+ const res = await fetch(`${apiUrl}/v1/register`, {
62
+ method: "POST",
63
+ headers: { "Content-Type": "application/json" },
64
+ body: JSON.stringify(body)
65
+ });
66
+ const json = await res.json();
67
+ if (!res.ok) {
68
+ const wrapped = json.data;
69
+ const errMsg = wrapped?.error ?? json.error ?? `HTTP ${res.status}`;
70
+ throw new Error(errMsg);
71
+ }
72
+ const data = json.data ?? json;
73
+ const token = data.registration_token;
74
+ if (!token) throw new Error("No registration_token in response");
75
+ return token;
76
+ }
77
+
78
+ // src/wait.ts
79
+ import { fetch as fetch2 } from "undici";
80
+ async function waitForApproval(apiUrl, token, timeoutMs, onTick) {
81
+ const deadline = Date.now() + timeoutMs;
82
+ const sseResult = await trySSE(apiUrl, token, Math.min(3e4, timeoutMs), onTick);
83
+ if (sseResult) return sseResult;
84
+ return poll(apiUrl, token, deadline, onTick);
85
+ }
86
+ async function trySSE(apiUrl, token, windowMs, onTick) {
87
+ const controller = new AbortController();
88
+ const timer = setTimeout(() => controller.abort(), windowMs);
89
+ try {
90
+ const res = await fetch2(`${apiUrl}/v1/register/stream/${token}`, {
91
+ signal: controller.signal,
92
+ headers: { Accept: "text/event-stream" }
93
+ });
94
+ if (!res.ok || !res.body) return null;
95
+ const reader = res.body.getReader();
96
+ const decoder = new TextDecoder();
97
+ let buffer = "";
98
+ while (true) {
99
+ const { done, value } = await reader.read();
100
+ if (done) break;
101
+ buffer += decoder.decode(value, { stream: true });
102
+ const lines = buffer.split("\n");
103
+ buffer = lines.pop() ?? "";
104
+ for (const line of lines) {
105
+ if (line.startsWith(":")) {
106
+ onTick?.();
107
+ continue;
108
+ }
109
+ if (line.startsWith("data:")) {
110
+ const dataStr = line.slice(5).trim();
111
+ if (!dataStr) continue;
112
+ try {
113
+ const data = JSON.parse(dataStr);
114
+ const parsed = parseEvent(data);
115
+ if (parsed) return parsed;
116
+ } catch {
117
+ }
118
+ }
119
+ }
120
+ }
121
+ return null;
122
+ } catch {
123
+ return null;
124
+ } finally {
125
+ clearTimeout(timer);
126
+ }
127
+ }
128
+ async function poll(apiUrl, token, deadline, onTick) {
129
+ while (Date.now() < deadline) {
130
+ await sleep(5e3);
131
+ onTick?.();
132
+ try {
133
+ const res = await fetch2(`${apiUrl}/v1/register/status/${token}`);
134
+ if (!res.ok) continue;
135
+ const json = await res.json();
136
+ const data = json.data ?? json;
137
+ const parsed = parseEvent(data);
138
+ if (parsed) return parsed;
139
+ } catch {
140
+ }
141
+ }
142
+ return { status: "timeout" };
143
+ }
144
+ function parseEvent(data) {
145
+ if (data.status === "approved") {
146
+ const agentId = data.agent_id ?? data.agentId;
147
+ if (!agentId) return null;
148
+ return { status: "approved", agentId };
149
+ }
150
+ if (data.status === "rejected") return { status: "rejected" };
151
+ if (data.status === "timeout" || data.status === "expired") return { status: "expired" };
152
+ return null;
153
+ }
154
+ function sleep(ms) {
155
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
156
+ }
157
+
158
+ // src/save.ts
159
+ import { writeFile, mkdir } from "fs/promises";
160
+ import { dirname, resolve } from "path";
161
+ async function saveOutput(opts) {
162
+ const keyPath = resolve(opts.keyFilePath);
163
+ const envPath = resolve(opts.envFilePath);
164
+ await mkdir(dirname(keyPath), { recursive: true });
165
+ await mkdir(dirname(envPath), { recursive: true });
166
+ await writeFile(keyPath, "", { mode: 384 });
167
+ const envContent = [
168
+ `# AgentValet \u2014 generated by agentvalet-register`,
169
+ `AGENTVALET_AGENT_ID=${opts.agentId}`,
170
+ `AGENTVALET_API_URL=${opts.apiUrl}`,
171
+ `AGENTVALET_PRIVATE_KEY_PATH=${keyPath}`,
172
+ ""
173
+ ].join("\n");
174
+ await writeFile(envPath, envContent, "utf-8");
175
+ return { keyPath, envPath };
176
+ }
177
+
178
+ // src/index.ts
179
+ function collectScope(value, previous) {
180
+ return [...previous, value];
181
+ }
182
+ var program = new Command();
183
+ program.name("agentvalet-register").description("Self-register an AI agent with AgentValet").version("0.1.0").option("--owner <uuid>", "Owner ID (or set AGENTVALET_OWNER env var)").option("--name <name>", "Agent name").option("--description <desc>", "Agent description").option("--scope <platform:scope>", "Requested scope, repeatable", collectScope, []).option("--api-url <url>", "AgentValet API base URL", "https://api.agentvalet.ai").option("--output <path>", "Path to write .env file", "./.env.agentvalet").option("--key-path <path>", "Path to write private key", "./agentvalet.key").option("--json", "Machine-readable JSON output").option("--no-wait", "Register only, do not wait for approval").option("--timeout <seconds>", "Approval wait timeout in seconds", "300").action(async (opts) => {
184
+ const isJson = !!opts.json;
185
+ let ownerId = opts.owner ?? process.env.AGENTVALET_OWNER ?? "";
186
+ let agentName = opts.name ?? "";
187
+ let description = opts.description ?? "";
188
+ let scopes = opts.scope;
189
+ if (!agentName) {
190
+ if (isJson) {
191
+ console.error(JSON.stringify({ error: "--name is required in non-interactive mode" }));
192
+ process.exit(3);
193
+ }
194
+ const answers = await runWizard({ ownerId, agentName, description, scopes });
195
+ ownerId = answers.ownerId;
196
+ agentName = answers.agentName;
197
+ description = answers.description;
198
+ scopes = answers.scopes;
199
+ }
200
+ if (!ownerId) {
201
+ if (isJson) {
202
+ console.error(JSON.stringify({ error: "--owner or AGENTVALET_OWNER is required" }));
203
+ process.exit(3);
204
+ }
205
+ console.error(chalk2.red("Error: --owner or AGENTVALET_OWNER environment variable is required"));
206
+ process.exit(3);
207
+ }
208
+ const scopeMap = /* @__PURE__ */ new Map();
209
+ for (const s of scopes) {
210
+ const colon = s.indexOf(":");
211
+ if (colon === -1) {
212
+ console.error(
213
+ isJson ? JSON.stringify({ error: `Invalid scope format: ${s} (expected platform:scope)` }) : chalk2.red(`Invalid scope format: ${s} (expected platform:scope)`)
214
+ );
215
+ process.exit(3);
216
+ }
217
+ const platform = s.slice(0, colon);
218
+ const scope = s.slice(colon + 1);
219
+ const existing = scopeMap.get(platform) ?? [];
220
+ scopeMap.set(platform, [...existing, scope]);
221
+ }
222
+ const requestedScopes = Array.from(scopeMap.entries()).map(([platformId, s]) => ({
223
+ platformId,
224
+ scopes: s
225
+ }));
226
+ let token;
227
+ try {
228
+ token = await registerAgent(opts.apiUrl, {
229
+ owner_id: ownerId,
230
+ agent_name: agentName,
231
+ agent_description: description || void 0,
232
+ requested_scopes: requestedScopes
233
+ });
234
+ } catch (err) {
235
+ const msg = err instanceof Error ? err.message : String(err);
236
+ console.error(isJson ? JSON.stringify({ error: msg }) : chalk2.red(`Registration failed: ${msg}`));
237
+ process.exit(3);
238
+ }
239
+ if (!isJson) {
240
+ console.log(chalk2.cyan(`
241
+ Approve at: https://agentvalet.ai/registrations
242
+ `));
243
+ }
244
+ if (!opts.wait) {
245
+ if (isJson) {
246
+ console.log(JSON.stringify({ status: "pending", token }));
247
+ } else {
248
+ console.log(chalk2.yellow("Registration submitted (--no-wait). Token:"), token);
249
+ }
250
+ process.exit(0);
251
+ }
252
+ const timeoutMs = parseInt(opts.timeout, 10) * 1e3;
253
+ let result;
254
+ try {
255
+ result = await waitForApproval(opts.apiUrl, token, timeoutMs, isJson ? void 0 : () => {
256
+ });
257
+ } catch (err) {
258
+ const msg = err instanceof Error ? err.message : String(err);
259
+ console.error(isJson ? JSON.stringify({ error: msg }) : chalk2.red(`Wait failed: ${msg}`));
260
+ process.exit(3);
261
+ }
262
+ if (result.status === "rejected") {
263
+ if (isJson) {
264
+ console.log(JSON.stringify({ status: "rejected" }));
265
+ } else {
266
+ console.log(chalk2.red("\nRegistration denied by owner."));
267
+ }
268
+ process.exit(1);
269
+ }
270
+ if (result.status === "timeout" || result.status === "expired") {
271
+ if (isJson) {
272
+ console.log(JSON.stringify({ status: "timeout" }));
273
+ } else {
274
+ console.log(chalk2.yellow("\nTimed out waiting for approval."));
275
+ }
276
+ process.exit(2);
277
+ }
278
+ const { agentId } = result;
279
+ let keyPath;
280
+ let envPath;
281
+ try {
282
+ ({ keyPath, envPath } = await saveOutput({
283
+ agentId,
284
+ apiUrl: opts.apiUrl,
285
+ keyFilePath: opts.keyPath,
286
+ envFilePath: opts.output
287
+ }));
288
+ } catch (err) {
289
+ const msg = err instanceof Error ? err.message : String(err);
290
+ console.error(isJson ? JSON.stringify({ error: msg }) : chalk2.red(`Failed to save output: ${msg}`));
291
+ process.exit(3);
292
+ }
293
+ if (isJson) {
294
+ console.log(JSON.stringify({ status: "approved", agentId, keyPath, envPath }));
295
+ } else {
296
+ console.log(chalk2.green(`
297
+ Approved! Agent ID: ${agentId}`));
298
+ console.log(chalk2.gray(`Key: ${keyPath}`));
299
+ console.log(chalk2.gray(`Env file: ${envPath}`));
300
+ console.log(`
301
+ Source the env file before calling any tool:`);
302
+ console.log(chalk2.cyan(` source ${envPath}`));
303
+ }
304
+ process.exit(0);
305
+ });
306
+ program.parseAsync().catch((err) => {
307
+ console.error(err);
308
+ process.exit(3);
309
+ });
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@agentvalet/register",
3
+ "version": "0.1.0",
4
+ "description": "Self-register AI agents with AgentValet",
5
+ "type": "module",
6
+ "bin": {
7
+ "agentvalet-register": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup src/index.ts --format esm --dts --clean",
15
+ "dev": "tsx src/index.ts",
16
+ "test": "vitest run",
17
+ "typecheck": "tsc --noEmit",
18
+ "prepublishOnly": "pnpm build"
19
+ },
20
+ "dependencies": {
21
+ "chalk": "^5.3.0",
22
+ "commander": "^12.0.0",
23
+ "ora": "^8.0.1",
24
+ "prompts": "^2.4.2",
25
+ "undici": "^6.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^20.0.0",
29
+ "@types/prompts": "^2.4.0",
30
+ "tsup": "^8.0.0",
31
+ "tsx": "^4.0.0",
32
+ "typescript": "^5.0.0",
33
+ "vitest": "^1.5.0"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/MCSEdwin/agentvalet.git",
41
+ "directory": "apps/cli"
42
+ },
43
+ "keywords": [
44
+ "ai",
45
+ "agents",
46
+ "auth",
47
+ "credentials",
48
+ "mcp",
49
+ "valet",
50
+ "agentvalet"
51
+ ],
52
+ "license": "MIT"
53
+ }