@agentvalet/register 0.1.0 → 0.1.2

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.
Files changed (2) hide show
  1. package/dist/index.js +197 -12
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -56,9 +56,9 @@ async function runWizard(prefilled) {
56
56
  }
57
57
 
58
58
  // src/register.ts
59
- import { fetch } from "undici";
59
+ import { fetch as fetch2 } from "undici";
60
60
  async function registerAgent(apiUrl, body) {
61
- const res = await fetch(`${apiUrl}/v1/register`, {
61
+ const res = await fetch2(`${apiUrl}/v1/register`, {
62
62
  method: "POST",
63
63
  headers: { "Content-Type": "application/json" },
64
64
  body: JSON.stringify(body)
@@ -76,18 +76,30 @@ async function registerAgent(apiUrl, body) {
76
76
  }
77
77
 
78
78
  // src/wait.ts
79
- import { fetch as fetch2 } from "undici";
79
+ import { fetch as fetch3 } from "undici";
80
80
  async function waitForApproval(apiUrl, token, timeoutMs, onTick) {
81
81
  const deadline = Date.now() + timeoutMs;
82
82
  const sseResult = await trySSE(apiUrl, token, Math.min(3e4, timeoutMs), onTick);
83
- if (sseResult) return sseResult;
84
- return poll(apiUrl, token, deadline, onTick);
83
+ if (sseResult) return enrichWithKey(apiUrl, token, sseResult);
84
+ const pollResult = await poll(apiUrl, token, deadline, onTick);
85
+ return enrichWithKey(apiUrl, token, pollResult);
86
+ }
87
+ async function enrichWithKey(apiUrl, token, result) {
88
+ if (result.status !== "approved") return result;
89
+ try {
90
+ const res = await fetch3(`${apiUrl}/v1/register/claim-key/${token}`);
91
+ if (!res.ok) return result;
92
+ const json = await res.json();
93
+ return { ...result, privateKey: json.private_key ?? "" };
94
+ } catch {
95
+ return result;
96
+ }
85
97
  }
86
98
  async function trySSE(apiUrl, token, windowMs, onTick) {
87
99
  const controller = new AbortController();
88
100
  const timer = setTimeout(() => controller.abort(), windowMs);
89
101
  try {
90
- const res = await fetch2(`${apiUrl}/v1/register/stream/${token}`, {
102
+ const res = await fetch3(`${apiUrl}/v1/register/stream/${token}`, {
91
103
  signal: controller.signal,
92
104
  headers: { Accept: "text/event-stream" }
93
105
  });
@@ -130,7 +142,7 @@ async function poll(apiUrl, token, deadline, onTick) {
130
142
  await sleep(5e3);
131
143
  onTick?.();
132
144
  try {
133
- const res = await fetch2(`${apiUrl}/v1/register/status/${token}`);
145
+ const res = await fetch3(`${apiUrl}/v1/register/status/${token}`);
134
146
  if (!res.ok) continue;
135
147
  const json = await res.json();
136
148
  const data = json.data ?? json;
@@ -152,7 +164,7 @@ function parseEvent(data) {
152
164
  return null;
153
165
  }
154
166
  function sleep(ms) {
155
- return new Promise((resolve2) => setTimeout(resolve2, ms));
167
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
156
168
  }
157
169
 
158
170
  // src/save.ts
@@ -163,10 +175,11 @@ async function saveOutput(opts) {
163
175
  const envPath = resolve(opts.envFilePath);
164
176
  await mkdir(dirname(keyPath), { recursive: true });
165
177
  await mkdir(dirname(envPath), { recursive: true });
166
- await writeFile(keyPath, "", { mode: 384 });
178
+ await writeFile(keyPath, opts.privateKey, { mode: 384 });
167
179
  const envContent = [
168
180
  `# AgentValet \u2014 generated by agentvalet-register`,
169
181
  `AGENTVALET_AGENT_ID=${opts.agentId}`,
182
+ `AGENTVALET_OWNER_ID=${opts.ownerId}`,
170
183
  `AGENTVALET_API_URL=${opts.apiUrl}`,
171
184
  `AGENTVALET_PRIVATE_KEY_PATH=${keyPath}`,
172
185
  ""
@@ -175,6 +188,140 @@ async function saveOutput(opts) {
175
188
  return { keyPath, envPath };
176
189
  }
177
190
 
191
+ // src/commands/refresh.ts
192
+ import { readFileSync, writeFileSync, mkdirSync, statSync } from "fs";
193
+ import { createSign, createPrivateKey } from "crypto";
194
+ import { dirname as dirname2, resolve as resolve2 } from "path";
195
+
196
+ // src/lib/skill-writer.ts
197
+ function generatePermissionsFile(agentId, ownerId, permissions) {
198
+ const now = (/* @__PURE__ */ new Date()).toISOString();
199
+ const platformSections = permissions.length === 0 ? "_No platforms approved yet. Visit https://agentvalet.ai to connect platforms._" : permissions.map((p) => {
200
+ const scopeList = p.scopes.map((s) => ` - \`${s}\``).join("\n");
201
+ const approvalNote = p.requireApproval ? "\n > Requires human approval before executing.\n" : "";
202
+ return `### ${p.platformName} (\`${p.platformId}\`)
203
+
204
+ Approved scopes:
205
+ ${scopeList}
206
+ ${approvalNote}`;
207
+ }).join("\n\n");
208
+ return `# AgentValet Permissions
209
+
210
+ Agent ID: \`${agentId}\`
211
+ Owner ID: \`${ownerId}\`
212
+ Last refreshed: ${now}
213
+
214
+ ---
215
+
216
+ ## Approved Platforms
217
+
218
+ ${platformSections}
219
+
220
+ ---
221
+
222
+ ## How to call a platform
223
+
224
+ All platform calls **must** go through AgentValet \u2014 never call Slack, Gmail, Stripe etc. directly.
225
+
226
+ Copy the pattern from \`test-agent.mjs\` (already in this repo):
227
+
228
+ 1. Read the private key from \`agentvalet.key\`
229
+ 2. Sign a short-lived RS256 JWT: \`{ agent_id, owner_id, iat, exp: iat + 300 }\`
230
+ 3. POST to \`https://api.agentvalet.ai/v1/actions\`:
231
+
232
+ \`\`\`json
233
+ {
234
+ "platform": "<platformId>",
235
+ "endpoint": "<endpoint>",
236
+ "method": "POST",
237
+ "scope": "<scope>",
238
+ "data": { ... }
239
+ }
240
+ \`\`\`
241
+
242
+ Run \`node test-agent.mjs\` for a live smoke-test.
243
+ To refresh this file: \`agentvalet-register refresh\`
244
+ `;
245
+ }
246
+
247
+ // src/commands/refresh.ts
248
+ function base64url(input) {
249
+ return Buffer.from(input).toString("base64url");
250
+ }
251
+ function signJwt(agentId, ownerId, privateKeyPem) {
252
+ const now = Math.floor(Date.now() / 1e3);
253
+ const header = base64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
254
+ const payload = base64url(JSON.stringify({ agent_id: agentId, owner_id: ownerId, iat: now, exp: now + 300 }));
255
+ const signingInput = `${header}.${payload}`;
256
+ const sign = createSign("RSA-SHA256");
257
+ sign.update(signingInput);
258
+ const signature = sign.sign(createPrivateKey(privateKeyPem), "base64url");
259
+ return `${signingInput}.${signature}`;
260
+ }
261
+ function parseEnvFile(content) {
262
+ const result = {};
263
+ for (const line of content.split("\n")) {
264
+ const trimmed = line.trim();
265
+ if (!trimmed || trimmed.startsWith("#")) continue;
266
+ const eq = trimmed.indexOf("=");
267
+ if (eq === -1) continue;
268
+ result[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
269
+ }
270
+ return result;
271
+ }
272
+ async function runRefresh(options) {
273
+ const outputPath = resolve2(options.outputPath);
274
+ if (options.ifStale) {
275
+ try {
276
+ const stat = statSync(outputPath);
277
+ if (Date.now() - stat.mtimeMs < 36e5) {
278
+ return;
279
+ }
280
+ } catch {
281
+ }
282
+ }
283
+ const envPath = resolve2(options.envFile);
284
+ let envVars;
285
+ try {
286
+ envVars = parseEnvFile(readFileSync(envPath, "utf-8"));
287
+ } catch {
288
+ throw new Error(`Cannot read env file: ${envPath}
289
+ Run 'agentvalet-register' first to create it.`);
290
+ }
291
+ const agentId = envVars["AGENTVALET_AGENT_ID"];
292
+ const ownerId = envVars["AGENTVALET_OWNER_ID"];
293
+ const apiUrl = envVars["AGENTVALET_API_URL"] ?? "https://api.agentvalet.ai";
294
+ const keyPath = envVars["AGENTVALET_PRIVATE_KEY_PATH"];
295
+ if (!agentId || !ownerId || !keyPath) {
296
+ throw new Error(`Env file is missing required keys (AGENTVALET_AGENT_ID, AGENTVALET_OWNER_ID, AGENTVALET_PRIVATE_KEY_PATH)`);
297
+ }
298
+ let privateKeyPem;
299
+ try {
300
+ privateKeyPem = readFileSync(keyPath, "utf-8");
301
+ } catch {
302
+ throw new Error(`Cannot read private key: ${keyPath}`);
303
+ }
304
+ if (!privateKeyPem.trim()) {
305
+ throw new Error(`Private key file is empty: ${keyPath}`);
306
+ }
307
+ const jwt = signJwt(agentId, ownerId, privateKeyPem);
308
+ const res = await fetch(`${apiUrl}/v1/agent/permissions`, {
309
+ headers: { Authorization: `Bearer ${jwt}` }
310
+ });
311
+ if (!res.ok) {
312
+ const body = await res.text().catch(() => "");
313
+ throw new Error(`Permissions API returned ${res.status}: ${body}`);
314
+ }
315
+ const json = await res.json();
316
+ const platforms = json.data?.platforms ?? [];
317
+ const content = generatePermissionsFile(agentId, ownerId, platforms);
318
+ mkdirSync(dirname2(outputPath), { recursive: true });
319
+ writeFileSync(outputPath, content, "utf-8");
320
+ if (!options.quiet) {
321
+ console.log(`Permissions refreshed \u2192 ${outputPath} (${platforms.length} platform${platforms.length !== 1 ? "s" : ""})`);
322
+ }
323
+ }
324
+
178
325
  // src/index.ts
179
326
  function collectScope(value, previous) {
180
327
  return [...previous, value];
@@ -275,12 +422,14 @@ Approve at: https://agentvalet.ai/registrations
275
422
  }
276
423
  process.exit(2);
277
424
  }
278
- const { agentId } = result;
425
+ const { agentId, privateKey } = result;
279
426
  let keyPath;
280
427
  let envPath;
281
428
  try {
282
429
  ({ keyPath, envPath } = await saveOutput({
283
430
  agentId,
431
+ ownerId,
432
+ privateKey: privateKey ?? "",
284
433
  apiUrl: opts.apiUrl,
285
434
  keyFilePath: opts.keyPath,
286
435
  envFilePath: opts.output
@@ -290,19 +439,55 @@ Approve at: https://agentvalet.ai/registrations
290
439
  console.error(isJson ? JSON.stringify({ error: msg }) : chalk2.red(`Failed to save output: ${msg}`));
291
440
  process.exit(3);
292
441
  }
442
+ const keyWritten = !!privateKey;
293
443
  if (isJson) {
294
- console.log(JSON.stringify({ status: "approved", agentId, keyPath, envPath }));
444
+ console.log(JSON.stringify({ status: "approved", agentId, keyPath: keyWritten ? keyPath : null, envPath }));
295
445
  } else {
296
446
  console.log(chalk2.green(`
297
447
  Approved! Agent ID: ${agentId}`));
298
- console.log(chalk2.gray(`Key: ${keyPath}`));
448
+ if (keyWritten) {
449
+ console.log(chalk2.gray(`Key: ${keyPath}`));
450
+ } else {
451
+ console.log(chalk2.yellow(`Key: not written \u2014 copy it from the dashboard and paste into ${keyPath}`));
452
+ }
299
453
  console.log(chalk2.gray(`Env file: ${envPath}`));
300
454
  console.log(`
301
455
  Source the env file before calling any tool:`);
302
456
  console.log(chalk2.cyan(` source ${envPath}`));
303
457
  }
458
+ if (keyWritten) {
459
+ try {
460
+ await runRefresh({
461
+ envFile: opts.output,
462
+ outputPath: ".claude/agentvalet-permissions.md",
463
+ ifStale: false,
464
+ quiet: isJson
465
+ });
466
+ if (!isJson) {
467
+ console.log(chalk2.gray(`Permissions: .claude/agentvalet-permissions.md`));
468
+ }
469
+ } catch {
470
+ if (!isJson) {
471
+ console.log(chalk2.yellow(`Permissions: run 'agentvalet-register refresh' to generate`));
472
+ }
473
+ }
474
+ }
304
475
  process.exit(0);
305
476
  });
477
+ program.command("refresh").description("Refresh approved permissions and update .claude/agentvalet-permissions.md").option("--env-file <path>", "Path to .env.agentvalet file", "./.env.agentvalet").option("--output <path>", "Path to write permissions file", "./.claude/agentvalet-permissions.md").option("--if-stale", "Skip if file is less than 1 hour old").option("--quiet", "Suppress output").action(async (opts) => {
478
+ try {
479
+ await runRefresh({
480
+ envFile: opts.envFile,
481
+ outputPath: opts.output,
482
+ ifStale: !!opts.ifStale,
483
+ quiet: !!opts.quiet
484
+ });
485
+ } catch (err) {
486
+ const msg = err instanceof Error ? err.message : String(err);
487
+ console.error(chalk2.red(`Refresh failed: ${msg}`));
488
+ process.exit(3);
489
+ }
490
+ });
306
491
  program.parseAsync().catch((err) => {
307
492
  console.error(err);
308
493
  process.exit(3);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvalet/register",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Self-register AI agents with AgentValet",
5
5
  "type": "module",
6
6
  "bin": {