@agentvalet/register 1.1.1 → 1.4.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.
Files changed (3) hide show
  1. package/README.md +21 -1
  2. package/dist/index.js +292 -183
  3. package/package.json +15 -13
package/README.md CHANGED
@@ -15,9 +15,29 @@ No installation required. Authenticate with GitHub, and your agent is registered
15
15
  1. Opens your browser to authenticate with GitHub
16
16
  2. Creates your AgentValet account (or finds your existing one)
17
17
  3. Registers this machine as an agent
18
- 4. Writes config to `.agentvalet/` in the current directory
18
+ 4. Writes config and your private key to `~/.agentvalet/` in your **home**
19
+ directory (not the current directory)
19
20
  5. Outputs the environment variables you need
20
21
 
22
+ ## Already have an agent on this machine?
23
+
24
+ An agent's private key exists only on your machine — the proxy never sees it,
25
+ and there is no rotation endpoint. Overwriting it destroys that identity, along
26
+ with its grants and audit continuity.
27
+
28
+ So registering while `~/.agentvalet/agent.key` already belongs to a **different**
29
+ agent stops with an error rather than overwriting it. You have two options:
30
+
31
+ ```bash
32
+ # Keep both — store the new agent under ~/.agentvalet/<label>/
33
+ npx @agentvalet/register claim <bind_secret> --label work-laptop
34
+
35
+ # Replace the existing one — a timestamped backup is written first
36
+ npx @agentvalet/register --force
37
+ ```
38
+
39
+ Re-registering the *same* agent over its own key is always allowed.
40
+
21
41
  ## Requirements
22
42
 
23
43
  - Node.js 18+
package/dist/index.js CHANGED
@@ -180,8 +180,72 @@ function sleep(ms) {
180
180
 
181
181
  // src/save.ts
182
182
  import { writeFile, mkdir } from "fs/promises";
183
+
184
+ // src/config/key-guard.ts
185
+ import { existsSync, copyFileSync, readFileSync } from "fs";
186
+ import { dirname, join } from "path";
187
+ var KeyClobberError = class extends Error {
188
+ constructor(keyPath, existingAgentId, message) {
189
+ super(message);
190
+ this.keyPath = keyPath;
191
+ this.existingAgentId = existingAgentId;
192
+ this.name = "KeyClobberError";
193
+ }
194
+ keyPath;
195
+ existingAgentId;
196
+ };
197
+ function existingKeyOwner(keyPath) {
198
+ if (!existsSync(keyPath)) return null;
199
+ const configPath = join(dirname(keyPath), "config.json");
200
+ if (!existsSync(configPath)) return null;
201
+ try {
202
+ const parsed = JSON.parse(readFileSync(configPath, "utf8"));
203
+ const id = parsed.agentId ?? parsed.lastAgentId;
204
+ return typeof id === "string" && id ? id : null;
205
+ } catch {
206
+ return null;
207
+ }
208
+ }
209
+ function backupExistingKey(keyPath) {
210
+ if (!existsSync(keyPath)) return null;
211
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
212
+ let backup = `${keyPath}.bak.${stamp}`;
213
+ let n = 1;
214
+ while (existsSync(backup)) backup = `${keyPath}.bak.${stamp}-${n++}`;
215
+ copyFileSync(keyPath, backup);
216
+ return backup;
217
+ }
218
+ function assertSafeToWriteKey(keyPath, incomingAgentId, opts = {}) {
219
+ if (!existsSync(keyPath)) return;
220
+ const owner = existingKeyOwner(keyPath);
221
+ if (owner && owner === incomingAgentId) return;
222
+ if (opts.force) {
223
+ backupExistingKey(keyPath);
224
+ return;
225
+ }
226
+ const whose = owner ? `agent ${owner}` : "another agent (owner unknown)";
227
+ throw new KeyClobberError(
228
+ keyPath,
229
+ owner,
230
+ [
231
+ `${keyPath} already holds the private key for ${whose}.`,
232
+ "",
233
+ "Overwriting it would destroy that identity permanently: the key exists",
234
+ "only on this machine and there is no way to recover or rotate it.",
235
+ "",
236
+ "Options:",
237
+ ` \u2022 Keep both \u2014 move ${keyPath} aside first, or use 'claim --label <name>'`,
238
+ " to store an additional agent under ~/.agentvalet/<name>/.",
239
+ " \u2022 Replace it \u2014 re-run with --force. The existing key is copied to",
240
+ ` ${keyPath}.bak.<timestamp> before being replaced.`
241
+ ].join("\n")
242
+ );
243
+ }
244
+
245
+ // src/save.ts
183
246
  async function saveOutput(opts) {
184
247
  await mkdir(AGENT_DIR, { recursive: true });
248
+ assertSafeToWriteKey(AGENT_KEY_PATH, opts.agentId, { force: opts.force });
185
249
  await writeFile(AGENT_KEY_PATH, opts.privateKey, { mode: 384 });
186
250
  saveConfig({
187
251
  agentId: opts.agentId,
@@ -196,8 +260,8 @@ async function saveOutput(opts) {
196
260
  }
197
261
 
198
262
  // src/commands/refresh.ts
199
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync2, statSync } from "fs";
200
- import { dirname, resolve as resolve2 } from "path";
263
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync2, statSync } from "fs";
264
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
201
265
 
202
266
  // src/lib/jwt.ts
203
267
  import { createSign, createPrivateKey } from "crypto";
@@ -215,13 +279,13 @@ function signJwt(agentId, ownerId, privateKeyPem) {
215
279
  }
216
280
 
217
281
  // src/lib/skill-writer.ts
218
- import { existsSync, writeFileSync, mkdirSync } from "fs";
282
+ import { existsSync as existsSync2, writeFileSync, mkdirSync } from "fs";
219
283
  import { execSync } from "child_process";
220
284
  import path from "path";
221
285
  import chalk2 from "chalk";
222
286
  function writeAgentsMd() {
223
287
  const dest = path.join(process.cwd(), "AGENTS.md");
224
- if (existsSync(dest)) {
288
+ if (existsSync2(dest)) {
225
289
  console.log(chalk2.dim(" AGENTS.md already exists \u2014 skipping"));
226
290
  return;
227
291
  }
@@ -232,15 +296,18 @@ Never call platform APIs (Slack, Stripe, Gmail, GitHub, etc.) directly.
232
296
 
233
297
  ## Making a platform call
234
298
 
235
- Use the \`use_platform\` MCP tool:
299
+ Pick the tool that matches what you are doing:
300
+
301
+ - \`read_platform\` to read (always GET)
302
+ - \`write_platform\` to create or update (POST, PUT or PATCH)
303
+ - \`delete_platform\` to remove (always DELETE)
236
304
 
237
305
  \`\`\`
238
- use_platform({
306
+ write_platform({
239
307
  platform: "slack",
240
308
  endpoint: "/api/chat.postMessage",
241
- method: "POST",
242
309
  scope: "slack:write",
243
- data: { channel: "#general", text: "Hello" }
310
+ body: { channel: "#general", text: "Hello" }
244
311
  })
245
312
  \`\`\`
246
313
 
@@ -261,7 +328,7 @@ Do not call platform APIs directly. Tell the user:
261
328
  }
262
329
  function writeSkillMd() {
263
330
  const dest = path.join(process.cwd(), "SKILL.md");
264
- if (existsSync(dest)) {
331
+ if (existsSync2(dest)) {
265
332
  console.log(chalk2.dim(" SKILL.md already exists \u2014 skipping"));
266
333
  return;
267
334
  }
@@ -271,15 +338,16 @@ Routes all platform API calls through AgentValet's credential proxy.
271
338
 
272
339
  ## Making platform calls
273
340
 
274
- Use the \`use_platform\` tool instead of calling APIs directly:
341
+ Use \`read_platform\`, \`write_platform\` or \`delete_platform\` instead of
342
+ calling APIs directly. Reads use read_platform; creates and updates use
343
+ write_platform; deletions use delete_platform.
275
344
 
276
345
  \`\`\`
277
- use_platform({
346
+ write_platform({
278
347
  platform: "slack",
279
348
  endpoint: "/api/chat.postMessage",
280
- method: "POST",
281
349
  scope: "slack:write",
282
- data: { channel: "#general", text: "Hello" }
350
+ body: { channel: "#general", text: "Hello" }
283
351
  })
284
352
  \`\`\`
285
353
 
@@ -298,7 +366,7 @@ Dashboard: https://app.agentvalet.ai
298
366
  }
299
367
  function writeQuickstartDoc(opts) {
300
368
  const dest = path.join(process.cwd(), "AGENTVALET.md");
301
- if (existsSync(dest)) {
369
+ if (existsSync2(dest)) {
302
370
  console.log(chalk2.dim(" AGENTVALET.md already exists \u2014 skipping"));
303
371
  return;
304
372
  }
@@ -381,9 +449,9 @@ AGENT_PRIVATE_KEY_PATH=${keyPath}
381
449
  }
382
450
  function writeFactoryDroidDroid() {
383
451
  const droidsDir = path.join(process.cwd(), ".factory", "droids");
384
- if (!existsSync(droidsDir)) mkdirSync(droidsDir, { recursive: true });
452
+ if (!existsSync2(droidsDir)) mkdirSync(droidsDir, { recursive: true });
385
453
  const dest = path.join(droidsDir, "agentvalet.yaml");
386
- if (existsSync(dest)) {
454
+ if (existsSync2(dest)) {
387
455
  console.log(chalk2.dim(" Factory Droid: .factory/droids/agentvalet.yaml already exists"));
388
456
  return;
389
457
  }
@@ -410,7 +478,8 @@ instructions: |
410
478
  ## Rules \u2014 read before every platform call
411
479
 
412
480
  1. NEVER call platform APIs (Slack, Stripe, Gmail, GitHub, etc.) directly.
413
- Always use the \`use_platform\` MCP tool.
481
+ Always use \`read_platform\` (reads), \`write_platform\` (creates and
482
+ updates) or \`delete_platform\` (deletions).
414
483
 
415
484
  2. Call \`list_platforms\` at the start of any session that involves
416
485
  platform calls. Do not assume platforms are available.
@@ -418,7 +487,7 @@ instructions: |
418
487
  3. Call \`authzen_evaluate\` before any destructive operation
419
488
  (delete, send, charge, publish). Do not proceed if decision=false.
420
489
 
421
- 4. If \`use_platform\` returns \`{ status: "pending_approval" }\`,
490
+ 4. If a platform call returns \`{ status: "pending_approval" }\`,
422
491
  wait for human approval. Do not retry the call.
423
492
 
424
493
  5. If AgentValet MCP tools are unavailable, stop and tell the user:
@@ -428,12 +497,11 @@ instructions: |
428
497
  ## Making a platform call
429
498
 
430
499
  \`\`\`
431
- use_platform({
500
+ write_platform({
432
501
  platform: "slack",
433
502
  endpoint: "/api/chat.postMessage",
434
- method: "POST",
435
503
  scope: "slack:write",
436
- data: { channel: "#general", text: "Hello" }
504
+ body: { channel: "#general", text: "Hello" }
437
505
  })
438
506
  \`\`\`
439
507
 
@@ -453,9 +521,9 @@ instructions: |
453
521
  }
454
522
  function writeFactoryDroidCommand() {
455
523
  const commandsDir = path.join(process.cwd(), ".factory", "commands");
456
- if (!existsSync(commandsDir)) mkdirSync(commandsDir, { recursive: true });
524
+ if (!existsSync2(commandsDir)) mkdirSync(commandsDir, { recursive: true });
457
525
  const statusDest = path.join(commandsDir, "av-status.md");
458
- if (!existsSync(statusDest)) {
526
+ if (!existsSync2(statusDest)) {
459
527
  const statusContent = `# /av-status
460
528
 
461
529
  Check your AgentValet agent status and approved platform connections.
@@ -478,7 +546,7 @@ Check your AgentValet agent status and approved platform connections.
478
546
  console.log(chalk2.dim(" Factory Droid: .factory/commands/av-status.md already exists"));
479
547
  }
480
548
  const registerDest = path.join(commandsDir, "av-register.md");
481
- if (!existsSync(registerDest)) {
549
+ if (!existsSync2(registerDest)) {
482
550
  const registerContent = `# /av-register
483
551
 
484
552
  Register or re-register this project with AgentValet.
@@ -505,9 +573,9 @@ After running, approve the agent at: https://app.agentvalet.ai/agents
505
573
  }
506
574
  function writeFactoryDroidReliabilityDroid() {
507
575
  const droidsDir = path.join(process.cwd(), ".factory", "droids");
508
- if (!existsSync(droidsDir)) mkdirSync(droidsDir, { recursive: true });
576
+ if (!existsSync2(droidsDir)) mkdirSync(droidsDir, { recursive: true });
509
577
  const dest = path.join(droidsDir, "agentvalet-reliability.yaml");
510
- if (existsSync(dest)) return;
578
+ if (existsSync2(dest)) return;
511
579
  const content = `# AgentValet Reliability Droid
512
580
  # Monitors agent call patterns and surfaces governance issues
513
581
  # Commit this file to your repo
@@ -540,7 +608,8 @@ instructions: |
540
608
  - If decision=false: block the call, explain why, suggest alternatives
541
609
  - If decision=true: allow, but log it
542
610
 
543
- 2. WHEN a droid calls \`use_platform\`, verify:
611
+ 2. WHEN a droid calls \`read_platform\`, \`write_platform\` or
612
+ \`delete_platform\`, verify:
544
613
  - The platform is in the approved list from \`list_platforms\`
545
614
  - The scope matches what was approved
546
615
  - The call hasn't exceeded rate limits
@@ -589,7 +658,8 @@ function ensureMcpServer() {
589
658
  function generatePermissionsFile(agentId, ownerId, permissions) {
590
659
  const now = (/* @__PURE__ */ new Date()).toISOString();
591
660
  const platformSections = permissions.length === 0 ? "_No platforms approved yet. Visit https://app.agentvalet.ai to connect platforms._" : permissions.map((p) => {
592
- const scopeList = p.scopes.map((s) => ` - \`${s}\``).join("\n");
661
+ const gated = new Set(p.approvalScopes ?? []);
662
+ const scopeList = p.scopes.map((s) => ` - \`${s}\`${gated.has(s) ? " \u2014 requires human approval" : ""}`).join("\n");
593
663
  const approvalNote = p.requireApproval ? "\n > Requires human approval before executing.\n" : "";
594
664
  return `### ${p.platformName} (\`${p.platformId}\`)
595
665
 
@@ -611,19 +681,58 @@ ${platformSections}
611
681
 
612
682
  ---
613
683
 
684
+ ## Before any platform call
685
+
686
+ 1. **Check AgentValet first.** For any action on an external platform (Slack,
687
+ GitHub, Stripe, Gmail, Outlook, Notion, Linear, Jira, HubSpot, Salesforce,
688
+ or any other SaaS), call \`mcp__agentvalet__list_platforms\` **before** you
689
+ assume the platform is unavailable or reach for another route.
690
+ 2. **If the platform is listed here, you must use it.** Do not call the
691
+ platform's API directly, and do not use another MCP server's tool for a
692
+ platform AgentValet covers \u2014 that bypasses the audit log, the approval
693
+ flow, and the scope limits the owner configured.
694
+ 3. **If it is not listed**, say so: "AgentValet doesn't cover <platform> yet."
695
+ Ask the user whether to request the integration from their AgentValet
696
+ owner. Do not silently fall back to direct API calls or raw keys found in
697
+ the environment.
698
+ 4. **Destructive scopes** (\`*.delete\`, \`stripe:charge\`, \`mail:send\`) need
699
+ human approval. Warn the user before invoking them.
700
+ 5. **On \`pending_approval\`** \u2014 the action is queued and runs when the owner
701
+ approves. Tell the user and **do not retry**; a retry queues a duplicate.
702
+ 6. **If the AgentValet MCP tools are unavailable**, stop. Tell the user
703
+ "AgentValet MCP is not connected. Run: npx @agentvalet/register". Do not
704
+ attempt the platform call without governance.
705
+
706
+ Read scopes are auto-approved. Write scopes may require approval. Destructive
707
+ scopes always require approval.
708
+
709
+ ---
710
+
614
711
  ## How to call a platform
615
712
 
616
- All platform calls **must** go through the \`mcp__agentvalet__use_platform\` MCP tool.
713
+ All platform calls **must** go through the AgentValet MCP tools. Pick the one
714
+ that matches the operation:
617
715
 
618
- **NEVER** write inline JWT signing code, \`node -e\` scripts, or manual HTTP calls. The MCP tool handles all authentication internally.
716
+ | Operation | Tool | Method |
717
+ |---|---|---|
718
+ | Read | \`mcp__agentvalet__read_platform\` | always GET |
719
+ | Create or update | \`mcp__agentvalet__write_platform\` | POST (default), PUT or PATCH |
720
+ | Delete | \`mcp__agentvalet__delete_platform\` | always DELETE |
721
+
722
+ **NEVER** write inline JWT signing code, \`node -e\` scripts, or manual HTTP calls. The MCP tools handle all authentication internally.
619
723
 
620
724
  \`\`\`
621
- mcp__agentvalet__use_platform({
725
+ mcp__agentvalet__read_platform({
726
+ platform: "<platformId>",
727
+ endpoint: "<endpoint>",
728
+ scope: "<scope>"
729
+ })
730
+
731
+ mcp__agentvalet__write_platform({
622
732
  platform: "<platformId>",
623
733
  endpoint: "<endpoint>",
624
- method: "POST",
625
734
  scope: "<scope>",
626
- data: { ... }
735
+ body: { ... }
627
736
  })
628
737
  \`\`\`
629
738
 
@@ -632,7 +741,7 @@ To refresh this file: \`npx @agentvalet/register refresh\`
632
741
  }
633
742
 
634
743
  // src/lib/require-agent.ts
635
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
744
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
636
745
  import { resolve } from "path";
637
746
  import chalk3 from "chalk";
638
747
  var NO_AGENT_MSG = chalk3.red("\n \u2717 No agent registered.\n") + chalk3.dim(
@@ -665,9 +774,9 @@ function tryResolveAgent(envFile = ".env.agentvalet") {
665
774
  if (!keyPath) keyPath = AGENT_KEY_PATH;
666
775
  if (!agentId || !ownerId) {
667
776
  const envPath = resolve(envFile);
668
- if (existsSync2(envPath)) {
777
+ if (existsSync3(envPath)) {
669
778
  try {
670
- const vars = parseEnvFile(readFileSync2(envPath, "utf-8"));
779
+ const vars = parseEnvFile(readFileSync3(envPath, "utf-8"));
671
780
  agentId = agentId ?? vars["AGENTVALET_AGENT_ID"];
672
781
  ownerId = ownerId ?? vars["AGENTVALET_OWNER_ID"];
673
782
  apiUrl = vars["AGENTVALET_API_URL"] ?? apiUrl;
@@ -681,12 +790,12 @@ function tryResolveAgent(envFile = ".env.agentvalet") {
681
790
  if (process.env["AGENTVALET_API_URL"]) apiUrl = process.env["AGENTVALET_API_URL"];
682
791
  if (process.env["AGENTVALET_PRIVATE_KEY_PATH"]) keyPath = process.env["AGENTVALET_PRIVATE_KEY_PATH"];
683
792
  if (!agentId || !ownerId || !keyPath) return null;
684
- if (!existsSync2(resolve(keyPath))) return null;
793
+ if (!existsSync3(resolve(keyPath))) return null;
685
794
  return { agentId, ownerId, keyPath, apiUrl };
686
795
  }
687
796
 
688
797
  // src/config/frameworks.ts
689
- import { existsSync as existsSync3 } from "fs";
798
+ import { existsSync as existsSync4 } from "fs";
690
799
  import { execSync as execSync2 } from "child_process";
691
800
  import os from "os";
692
801
  import path2 from "path";
@@ -704,27 +813,27 @@ function codexDesktopInstalled() {
704
813
  const home = os.homedir();
705
814
  const platform = process.platform;
706
815
  if (platform === "darwin") {
707
- return existsSync3("/Applications/Codex.app") || existsSync3(path2.join(home, "Applications", "Codex.app"));
816
+ return existsSync4("/Applications/Codex.app") || existsSync4(path2.join(home, "Applications", "Codex.app"));
708
817
  }
709
818
  if (platform === "win32") {
710
819
  const localAppData = process.env.LOCALAPPDATA ?? path2.join(home, "AppData", "Local");
711
820
  const programFiles = process.env["ProgramFiles"] ?? "C:\\Program Files";
712
- return existsSync3(path2.join(localAppData, "Programs", "codex")) || existsSync3(path2.join(localAppData, "Programs", "Codex")) || existsSync3(path2.join(programFiles, "Codex"));
821
+ return existsSync4(path2.join(localAppData, "Programs", "codex")) || existsSync4(path2.join(localAppData, "Programs", "Codex")) || existsSync4(path2.join(programFiles, "Codex"));
713
822
  }
714
- return existsSync3("/usr/share/applications/codex.desktop") || existsSync3(path2.join(home, ".local", "share", "applications", "codex.desktop"));
823
+ return existsSync4("/usr/share/applications/codex.desktop") || existsSync4(path2.join(home, ".local", "share", "applications", "codex.desktop"));
715
824
  }
716
825
  function claudeDesktopInstalled() {
717
826
  const home = os.homedir();
718
827
  const platform = process.platform;
719
828
  if (platform === "darwin") {
720
- return existsSync3("/Applications/Claude.app") || existsSync3(path2.join(home, "Library", "Application Support", "Claude"));
829
+ return existsSync4("/Applications/Claude.app") || existsSync4(path2.join(home, "Library", "Application Support", "Claude"));
721
830
  }
722
831
  if (platform === "win32") {
723
832
  const appData = process.env.APPDATA ?? path2.join(home, "AppData", "Roaming");
724
833
  const localAppData = process.env.LOCALAPPDATA ?? path2.join(home, "AppData", "Local");
725
- return existsSync3(path2.join(appData, "Claude")) || existsSync3(path2.join(localAppData, "AnthropicClaude")) || existsSync3(path2.join(localAppData, "Programs", "claude"));
834
+ return existsSync4(path2.join(appData, "Claude")) || existsSync4(path2.join(localAppData, "AnthropicClaude")) || existsSync4(path2.join(localAppData, "Programs", "claude"));
726
835
  }
727
- return existsSync3(path2.join(home, ".config", "Claude"));
836
+ return existsSync4(path2.join(home, ".config", "Claude"));
728
837
  }
729
838
  function cursorInstalled() {
730
839
  const home = os.homedir();
@@ -732,24 +841,24 @@ function cursorInstalled() {
732
841
  if (platform === "win32") {
733
842
  const appData = process.env.APPDATA ?? path2.join(home, "AppData", "Roaming");
734
843
  const localAppData = process.env.LOCALAPPDATA ?? path2.join(home, "AppData", "Local");
735
- return existsSync3(path2.join(appData, "Cursor")) || existsSync3(path2.join(localAppData, "Programs", "cursor"));
844
+ return existsSync4(path2.join(appData, "Cursor")) || existsSync4(path2.join(localAppData, "Programs", "cursor"));
736
845
  }
737
846
  if (platform === "darwin") {
738
- return existsSync3(path2.join(home, "Library", "Application Support", "Cursor")) || existsSync3("/Applications/Cursor.app");
847
+ return existsSync4(path2.join(home, "Library", "Application Support", "Cursor")) || existsSync4("/Applications/Cursor.app");
739
848
  }
740
- return existsSync3(path2.join(home, ".config", "Cursor")) || existsSync3("/usr/share/applications/cursor.desktop");
849
+ return existsSync4(path2.join(home, ".config", "Cursor")) || existsSync4("/usr/share/applications/cursor.desktop");
741
850
  }
742
851
  function detectFrameworks() {
743
852
  const home = os.homedir();
744
853
  return {
745
- codex: existsSync3(path2.join(home, ".codex")) || tryBinary("codex"),
854
+ codex: existsSync4(path2.join(home, ".codex")) || tryBinary("codex"),
746
855
  codexDesktop: codexDesktopInstalled(),
747
- claudeCode: existsSync3(path2.join(home, ".claude")) || tryBinary("claude"),
856
+ claudeCode: existsSync4(path2.join(home, ".claude")) || tryBinary("claude"),
748
857
  claudeDesktop: claudeDesktopInstalled(),
749
- cursor: existsSync3(path2.join(process.cwd(), ".cursor")) || cursorInstalled(),
858
+ cursor: existsSync4(path2.join(process.cwd(), ".cursor")) || cursorInstalled(),
750
859
  cursorCli: tryBinary("cursor-agent"),
751
- openclaw: existsSync3(path2.join(home, ".openclaw")),
752
- factoryDroid: existsSync3(path2.join(home, ".factory")) || existsSync3(path2.join(process.cwd(), ".factory"))
860
+ openclaw: existsSync4(path2.join(home, ".openclaw")),
861
+ factoryDroid: existsSync4(path2.join(home, ".factory")) || existsSync4(path2.join(process.cwd(), ".factory"))
753
862
  };
754
863
  }
755
864
  function resolveAgentType(detected) {
@@ -816,22 +925,55 @@ var CLAUDE_START = "<!-- agentvalet:start -->";
816
925
  var CLAUDE_END = "<!-- agentvalet:end -->";
817
926
  var HOOK_COMMAND = "npx @agentvalet/register refresh --if-stale --quiet 2>/dev/null || true";
818
927
  var HOOK_MARKER = "agentvalet/register refresh";
928
+ function projectRootFor(outputPath) {
929
+ return dirname2(dirname2(resolve2(outputPath)));
930
+ }
819
931
  function writePermissionsFromEvent(agentId, ownerId, platforms, outputPath = "./.claude/agentvalet-permissions.md") {
820
932
  const content = generatePermissionsFile(agentId, ownerId, platforms);
821
933
  const resolvedPath = resolve2(outputPath);
822
- mkdirSync2(dirname(resolvedPath), { recursive: true });
934
+ const projectRoot = projectRootFor(resolvedPath);
935
+ mkdirSync2(dirname2(resolvedPath), { recursive: true });
823
936
  writeFileSync2(resolvedPath, content, "utf-8");
824
- embedInClaudeMd(content);
825
- ensureHook();
937
+ embedInClaudeMd(content, projectRoot);
938
+ ensureHook(projectRoot);
939
+ }
940
+ async function writeGovernanceInstructions(agentId, ownerId) {
941
+ const writeRulesOnly = () => {
942
+ try {
943
+ writePermissionsFromEvent(agentId, ownerId, []);
944
+ console.log(" \u2713 CLAUDE.md \u2014 AgentValet governance rules");
945
+ return true;
946
+ } catch {
947
+ return false;
948
+ }
949
+ };
950
+ if (!tryResolveAgent()) {
951
+ writeRulesOnly();
952
+ return;
953
+ }
954
+ try {
955
+ await runRefresh({
956
+ envFile: ".env.agentvalet",
957
+ outputPath: ".claude/agentvalet-permissions.md",
958
+ ifStale: false,
959
+ quiet: true
960
+ });
961
+ console.log(" \u2713 CLAUDE.md \u2014 AgentValet governance rules and approved platforms");
962
+ } catch {
963
+ if (writeRulesOnly()) {
964
+ console.log(" Run `npx @agentvalet/register refresh` once approved to fill in platforms");
965
+ }
966
+ }
826
967
  }
827
968
  async function runRefresh(options) {
828
969
  const outputPath = resolve2(options.outputPath);
970
+ const projectRoot = projectRootFor(outputPath);
829
971
  if (options.ifStale) {
830
972
  try {
831
973
  const stat = statSync(outputPath);
832
974
  const fileIsFresh = Date.now() - stat.mtimeMs < 36e5;
833
- const claudeMdPath = resolve2("CLAUDE.md");
834
- const claudeMd = existsSync4(claudeMdPath) ? readFileSync3(claudeMdPath, "utf-8") : "";
975
+ const claudeMdPath = join2(projectRoot, "CLAUDE.md");
976
+ const claudeMd = existsSync5(claudeMdPath) ? readFileSync4(claudeMdPath, "utf-8") : "";
835
977
  const blockStart = claudeMd.indexOf(CLAUDE_START);
836
978
  const blockEnd = claudeMd.indexOf(CLAUDE_END);
837
979
  const blockContent = blockStart !== -1 && blockEnd !== -1 ? claudeMd.slice(blockStart, blockEnd) : "";
@@ -841,14 +983,14 @@ async function runRefresh(options) {
841
983
  }
842
984
  }
843
985
  const creds = tryResolveAgent(options.envFile);
844
- if (!creds || !existsSync4(resolve2(creds.keyPath))) {
986
+ if (!creds || !existsSync5(resolve2(creds.keyPath))) {
845
987
  console.error(" \u2717 No agent registered.\n Run the following command first to register an agent in this directory:\n\n npx @agentvalet/register\n");
846
988
  process.exit(1);
847
989
  }
848
990
  const { agentId, ownerId, keyPath, apiUrl } = creds;
849
991
  let privateKeyPem;
850
992
  try {
851
- privateKeyPem = readFileSync3(keyPath, "utf-8");
993
+ privateKeyPem = readFileSync4(keyPath, "utf-8");
852
994
  } catch {
853
995
  throw new Error(`Cannot read private key: ${keyPath}`);
854
996
  }
@@ -866,17 +1008,17 @@ async function runRefresh(options) {
866
1008
  const json = await res.json();
867
1009
  const platforms = json.data?.platforms ?? [];
868
1010
  const content = generatePermissionsFile(agentId, ownerId, platforms);
869
- mkdirSync2(dirname(outputPath), { recursive: true });
1011
+ mkdirSync2(dirname2(outputPath), { recursive: true });
870
1012
  writeFileSync2(outputPath, content, "utf-8");
871
- embedInClaudeMd(content);
872
- ensureHook();
1013
+ embedInClaudeMd(content, projectRoot);
1014
+ ensureHook(projectRoot);
873
1015
  const detected = detectFrameworks();
874
1016
  const refreshed = [];
875
1017
  if (detected.codex || detected.codexDesktop) {
876
- if (embedPermissionsBlock(resolve2("AGENTS.md"), content)) refreshed.push("AGENTS.md");
1018
+ if (embedPermissionsBlock(join2(projectRoot, "AGENTS.md"), content)) refreshed.push("AGENTS.md");
877
1019
  }
878
1020
  if (detected.openclaw) {
879
- if (embedPermissionsBlock(resolve2("SKILL.md"), content)) refreshed.push("SKILL.md");
1021
+ if (embedPermissionsBlock(join2(projectRoot, "SKILL.md"), content)) refreshed.push("SKILL.md");
880
1022
  }
881
1023
  if (!options.quiet) {
882
1024
  const extras = refreshed.length > 0 ? ` (also: ${refreshed.join(", ")})` : "";
@@ -884,9 +1026,9 @@ async function runRefresh(options) {
884
1026
  }
885
1027
  }
886
1028
  function embedPermissionsBlock(filePath, permissionsContent) {
887
- if (!existsSync4(filePath)) return false;
1029
+ if (!existsSync5(filePath)) return false;
888
1030
  try {
889
- const existing = readFileSync3(filePath, "utf-8");
1031
+ const existing = readFileSync4(filePath, "utf-8");
890
1032
  const block = `${PERMISSIONS_START}
891
1033
  ${permissionsContent.trimEnd()}
892
1034
  ${PERMISSIONS_END}`;
@@ -908,19 +1050,19 @@ ${block}
908
1050
  return false;
909
1051
  }
910
1052
  }
911
- function embedInClaudeMd(permissionsContent) {
912
- const claudeMdPath = resolve2("CLAUDE.md");
1053
+ function embedInClaudeMd(permissionsContent, projectRoot) {
1054
+ const claudeMdPath = join2(projectRoot, "CLAUDE.md");
913
1055
  const block = `${CLAUDE_START}
914
1056
  ${permissionsContent.trimEnd()}
915
1057
  ${CLAUDE_END}`;
916
- if (!existsSync4(claudeMdPath)) {
1058
+ if (!existsSync5(claudeMdPath)) {
917
1059
  writeFileSync2(claudeMdPath, `# Agent Instructions
918
1060
 
919
1061
  ${block}
920
1062
  `, "utf-8");
921
1063
  return;
922
1064
  }
923
- const existing = readFileSync3(claudeMdPath, "utf-8");
1065
+ const existing = readFileSync4(claudeMdPath, "utf-8");
924
1066
  if (existing.includes(CLAUDE_START)) {
925
1067
  const replaced = existing.replace(
926
1068
  new RegExp(`${CLAUDE_START}[\\s\\S]*?${CLAUDE_END}`, "m"),
@@ -935,12 +1077,12 @@ ${block}
935
1077
  `, "utf-8");
936
1078
  }
937
1079
  }
938
- function ensureHook() {
939
- const settingsPath = resolve2(".claude/settings.local.json");
1080
+ function ensureHook(projectRoot) {
1081
+ const settingsPath = join2(projectRoot, ".claude", "settings.local.json");
940
1082
  let settings = {};
941
- if (existsSync4(settingsPath)) {
1083
+ if (existsSync5(settingsPath)) {
942
1084
  try {
943
- settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
1085
+ settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
944
1086
  } catch {
945
1087
  return;
946
1088
  }
@@ -956,22 +1098,28 @@ function ensureHook() {
956
1098
  }
957
1099
  const perms = settings.permissions ?? {};
958
1100
  const allow = perms.allow ?? [];
959
- const mcpAllows = ["mcp__agentvalet__list_platforms", "mcp__agentvalet__use_platform"];
1101
+ const mcpAllows = [
1102
+ "mcp__agentvalet__list_platforms",
1103
+ "mcp__agentvalet__read_platform",
1104
+ "mcp__agentvalet__write_platform",
1105
+ "mcp__agentvalet__delete_platform",
1106
+ "mcp__agentvalet__use_platform"
1107
+ ];
960
1108
  const missing = mcpAllows.filter((e) => !allow.includes(e));
961
1109
  if (missing.length > 0) {
962
1110
  perms.allow = [...allow, ...missing];
963
1111
  settings.permissions = perms;
964
1112
  }
965
- mkdirSync2(dirname(settingsPath), { recursive: true });
1113
+ mkdirSync2(dirname2(settingsPath), { recursive: true });
966
1114
  writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
967
- ensureMcpJson();
1115
+ ensureMcpJson(projectRoot);
968
1116
  }
969
- function ensureMcpJson() {
970
- const mcpJsonPath = resolve2(".mcp.json");
1117
+ function ensureMcpJson(projectRoot) {
1118
+ const mcpJsonPath = join2(projectRoot, ".mcp.json");
971
1119
  let mcpConfig = {};
972
- if (existsSync4(mcpJsonPath)) {
1120
+ if (existsSync5(mcpJsonPath)) {
973
1121
  try {
974
- mcpConfig = JSON.parse(readFileSync3(mcpJsonPath, "utf-8"));
1122
+ mcpConfig = JSON.parse(readFileSync4(mcpJsonPath, "utf-8"));
975
1123
  } catch {
976
1124
  return;
977
1125
  }
@@ -1170,7 +1318,7 @@ async function writeLocalConfig(config) {
1170
1318
  // src/output/env.ts
1171
1319
  import chalk6 from "chalk";
1172
1320
  import { appendFile as appendFile2, writeFile as writeFile3 } from "fs/promises";
1173
- import { existsSync as existsSync5 } from "fs";
1321
+ import { existsSync as existsSync6 } from "fs";
1174
1322
  import path4 from "path";
1175
1323
  function formatEnvBlock(vars) {
1176
1324
  return Object.entries(vars).map(([key, value]) => {
@@ -1181,7 +1329,7 @@ function formatEnvBlock(vars) {
1181
1329
 
1182
1330
  // src/config/ide.ts
1183
1331
  import { mkdir as mkdir3, writeFile as writeFile4, readFile as readFile2, appendFile as appendFile3, copyFile } from "fs/promises";
1184
- import { existsSync as existsSync6 } from "fs";
1332
+ import { existsSync as existsSync7 } from "fs";
1185
1333
  import path5 from "path";
1186
1334
  import os2 from "os";
1187
1335
  function buildMcpEntry(params) {
@@ -1218,7 +1366,7 @@ function claudeDesktopFriendly() {
1218
1366
  }
1219
1367
  async function mergeJsonFile(filePath, key, value) {
1220
1368
  let existing = {};
1221
- if (existsSync6(filePath)) {
1369
+ if (existsSync7(filePath)) {
1222
1370
  try {
1223
1371
  const text = await readFile2(filePath, "utf8");
1224
1372
  existing = JSON.parse(text);
@@ -1230,48 +1378,6 @@ async function mergeJsonFile(filePath, key, value) {
1230
1378
  existing.mcpServers = mcpServers;
1231
1379
  await writeFile4(filePath, JSON.stringify(existing, null, 2) + "\n", "utf8");
1232
1380
  }
1233
- function buildClaudeMd(agentId, ownerId, proxyUrl) {
1234
- return `# AgentValet \u2014 Credential Governance
1235
-
1236
- Agent ID: \`${agentId}\`
1237
- Owner ID: \`${ownerId}\`
1238
-
1239
- All external platform API calls **must** go through the AgentValet proxy.
1240
- Never call platform APIs (Slack, Stripe, Gmail, GitHub, etc.) directly.
1241
-
1242
- ## Making a platform call
1243
-
1244
- Use the \`mcp__agentvalet__use_platform\` MCP tool:
1245
-
1246
- \`\`\`
1247
- mcp__agentvalet__use_platform({
1248
- platform: "slack",
1249
- endpoint: "/api/chat.postMessage",
1250
- method: "POST",
1251
- scope: "slack:write",
1252
- data: { channel: "#general", text: "Hello" }
1253
- })
1254
- \`\`\`
1255
-
1256
- ## Before any platform session
1257
-
1258
- 1. Call \`mcp__agentvalet__list_platforms\` to see approved platforms and scopes
1259
- 2. Call \`mcp__agentvalet__authzen_evaluate\` before destructive operations
1260
- 3. If a call returns \`pending_approval\` \u2014 wait, do not retry
1261
-
1262
- ## Rules
1263
-
1264
- - **NEVER** write inline JWT signing code, \`node -e\` scripts, or manual HTTP calls
1265
- - The MCP tool handles authentication automatically
1266
- - \`AGENT_PRIVATE_KEY_PATH\` points to your key \u2014 do not read or log it
1267
-
1268
- ## If AgentValet MCP is not connected
1269
-
1270
- Tell the user: "AgentValet MCP is not connected. Run: npx @agentvalet/register"
1271
-
1272
- Dashboard: ${proxyUrl.replace("api.", "app.")}
1273
- `;
1274
- }
1275
1381
  function buildCodexTomlBlock(params) {
1276
1382
  return `
1277
1383
  [mcp_servers.agentvalet]
@@ -1282,8 +1388,8 @@ env = { AGENT_ID = "${params.agentId}", OWNER_ID = "${params.ownerId}", PROXY_UR
1282
1388
  }
1283
1389
  async function appendCodexConfig(configPath) {
1284
1390
  const dir = path5.dirname(configPath);
1285
- if (!existsSync6(dir)) await mkdir3(dir, { recursive: true });
1286
- const existing = existsSync6(configPath) ? await readFile2(configPath, "utf8") : "";
1391
+ if (!existsSync7(dir)) await mkdir3(dir, { recursive: true });
1392
+ const existing = existsSync7(configPath) ? await readFile2(configPath, "utf8") : "";
1287
1393
  if (existing.includes("[mcp_servers.agentvalet]")) return false;
1288
1394
  return true;
1289
1395
  }
@@ -1292,7 +1398,7 @@ async function writeGlobalFrameworkConfigs(params) {
1292
1398
  const codexDir = path5.join(os2.homedir(), ".codex");
1293
1399
  const codexConfigPath = path5.join(codexDir, "config.toml");
1294
1400
  try {
1295
- if (!existsSync6(codexDir)) await mkdir3(codexDir, { recursive: true });
1401
+ if (!existsSync7(codexDir)) await mkdir3(codexDir, { recursive: true });
1296
1402
  if (await appendCodexConfig(codexConfigPath)) {
1297
1403
  await appendFile3(codexConfigPath, buildCodexTomlBlock(params), "utf8");
1298
1404
  written.push("~/.codex/config.toml");
@@ -1317,7 +1423,7 @@ async function writeCodexProjectConfig(params) {
1317
1423
  return written;
1318
1424
  }
1319
1425
  async function stripAgentvaletFromMcpJson(filePath) {
1320
- if (!existsSync6(filePath)) return false;
1426
+ if (!existsSync7(filePath)) return false;
1321
1427
  try {
1322
1428
  const text = await readFile2(filePath, "utf8");
1323
1429
  const config = JSON.parse(text);
@@ -1341,7 +1447,7 @@ async function cleanProjectScopedEntries() {
1341
1447
  cleaned.push(".cursor/mcp.json (project entry removed)");
1342
1448
  }
1343
1449
  const factoryProjectMcp = path5.join(cwd, ".factory/mcp.json");
1344
- if (existsSync6(factoryProjectMcp)) {
1450
+ if (existsSync7(factoryProjectMcp)) {
1345
1451
  try {
1346
1452
  const text = await readFile2(factoryProjectMcp, "utf8");
1347
1453
  const config = JSON.parse(text);
@@ -1407,17 +1513,6 @@ async function writeIdeConfigs(params) {
1407
1513
  } catch (err) {
1408
1514
  console.warn(` Warning: could not write .mcp.json: ${err.message}`);
1409
1515
  }
1410
- const claudeMdPath = path5.join(cwd, "CLAUDE.md");
1411
- if (!existsSync6(claudeMdPath)) {
1412
- try {
1413
- await writeFile4(claudeMdPath, buildClaudeMd(params.agentId, params.ownerId, params.proxyUrl), "utf8");
1414
- written.push("CLAUDE.md");
1415
- } catch (err) {
1416
- console.warn(` Warning: could not write CLAUDE.md: ${err.message}`);
1417
- }
1418
- } else {
1419
- written.push("CLAUDE.md (already exists \u2014 skipped)");
1420
- }
1421
1516
  }
1422
1517
  if (detected.cursor || detected.cursorCli) {
1423
1518
  const cursorDir = path5.join(cwd, ".cursor");
@@ -1443,7 +1538,7 @@ async function writeIdeConfigs(params) {
1443
1538
  }
1444
1539
  async function mergeFactoryDroidMcpJson(filePath, key, value) {
1445
1540
  let existing = {};
1446
- if (existsSync6(filePath)) {
1541
+ if (existsSync7(filePath)) {
1447
1542
  try {
1448
1543
  const text = await readFile2(filePath, "utf8");
1449
1544
  existing = JSON.parse(text);
@@ -1457,7 +1552,7 @@ async function mergeFactoryDroidMcpJson(filePath, key, value) {
1457
1552
  }
1458
1553
  async function updateGitignoreForFactoryDroid() {
1459
1554
  const gitignorePath = path5.join(process.cwd(), ".gitignore");
1460
- if (!existsSync6(gitignorePath)) return;
1555
+ if (!existsSync7(gitignorePath)) return;
1461
1556
  const existing = await readFile2(gitignorePath, "utf8");
1462
1557
  if (existing.includes(".factory/mcp.json")) return;
1463
1558
  const additions = [
@@ -1676,6 +1771,7 @@ async function registerAgentFlow(apiKey, ownerId, scope = "user", presetName) {
1676
1771
  detected
1677
1772
  });
1678
1773
  }
1774
+ await writeGovernanceInstructions(agent.agent_id, ownerId);
1679
1775
  ensureMcpServer();
1680
1776
  } else if (shouldSetup === false) {
1681
1777
  console.log("");
@@ -1838,8 +1934,8 @@ import chalk10 from "chalk";
1838
1934
  // src/commands/uninstall.ts
1839
1935
  import { Command as Command2 } from "commander";
1840
1936
  import chalk9 from "chalk";
1841
- import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync3, rmSync } from "fs";
1842
- import { resolve as resolve3, join } from "path";
1937
+ import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync3, rmSync } from "fs";
1938
+ import { resolve as resolve3, join as join3 } from "path";
1843
1939
  import { homedir } from "os";
1844
1940
  var CLAUDE_START2 = "<!-- agentvalet:start -->";
1845
1941
  var CLAUDE_END2 = "<!-- agentvalet:end -->";
@@ -1847,9 +1943,9 @@ var PERMISSIONS_START2 = "<!-- agentvalet:permissions:start -->";
1847
1943
  var PERMISSIONS_END2 = "<!-- agentvalet:permissions:end -->";
1848
1944
  var HOOK_MARKER2 = "agentvalet/register refresh";
1849
1945
  function stripPermissionsBlock(filePath) {
1850
- if (!existsSync7(filePath)) return false;
1946
+ if (!existsSync8(filePath)) return false;
1851
1947
  try {
1852
- const content = readFileSync4(filePath, "utf-8");
1948
+ const content = readFileSync5(filePath, "utf-8");
1853
1949
  if (!content.includes(PERMISSIONS_START2)) return false;
1854
1950
  const stripped = content.replace(new RegExp(`${PERMISSIONS_START2}[\\s\\S]*?${PERMISSIONS_END2}`, "m"), "").replace(/(\r?\n){3,}/g, "\n\n").trimEnd();
1855
1951
  writeFileSync3(filePath, stripped + "\n", "utf-8");
@@ -1860,8 +1956,8 @@ function stripPermissionsBlock(filePath) {
1860
1956
  }
1861
1957
  function stripClaudeMd() {
1862
1958
  const path8 = resolve3("CLAUDE.md");
1863
- if (!existsSync7(path8)) return;
1864
- let content = readFileSync4(path8, "utf-8");
1959
+ if (!existsSync8(path8)) return;
1960
+ let content = readFileSync5(path8, "utf-8");
1865
1961
  content = content.replace(new RegExp(`${CLAUDE_START2}[\\s\\S]*?${CLAUDE_END2}`, "m"), "");
1866
1962
  content = content.replace(/^@\.claude\/agentvalet-permissions\.md\s*$/m, "");
1867
1963
  content = content.replace(/(\r?\n){3,}/g, "\n\n").trim();
@@ -1869,9 +1965,9 @@ function stripClaudeMd() {
1869
1965
  }
1870
1966
  function stripSettingsJson() {
1871
1967
  const path8 = resolve3(".claude/settings.local.json");
1872
- if (!existsSync7(path8)) return;
1968
+ if (!existsSync8(path8)) return;
1873
1969
  try {
1874
- const settings = JSON.parse(readFileSync4(path8, "utf-8"));
1970
+ const settings = JSON.parse(readFileSync5(path8, "utf-8"));
1875
1971
  const hooks = settings.hooks ?? {};
1876
1972
  const ups = hooks.UserPromptSubmit ?? [];
1877
1973
  hooks.UserPromptSubmit = ups.filter(
@@ -1887,9 +1983,9 @@ function stripSettingsJson() {
1887
1983
  }
1888
1984
  }
1889
1985
  function stripMcpJson(filePath, options = {}) {
1890
- if (!existsSync7(filePath)) return false;
1986
+ if (!existsSync8(filePath)) return false;
1891
1987
  try {
1892
- const config = JSON.parse(readFileSync4(filePath, "utf-8"));
1988
+ const config = JSON.parse(readFileSync5(filePath, "utf-8"));
1893
1989
  let removed = false;
1894
1990
  if (options.wrapped === false) {
1895
1991
  if (config["agentvalet"] !== void 0) {
@@ -1913,9 +2009,9 @@ function stripMcpJson(filePath, options = {}) {
1913
2009
  }
1914
2010
  }
1915
2011
  function stripCodexTomlAt(filePath) {
1916
- if (!existsSync7(filePath)) return false;
2012
+ if (!existsSync8(filePath)) return false;
1917
2013
  try {
1918
- const content = readFileSync4(filePath, "utf-8");
2014
+ const content = readFileSync5(filePath, "utf-8");
1919
2015
  if (!content.includes("[mcp_servers.agentvalet]")) return false;
1920
2016
  const stripped = content.replace(
1921
2017
  /\r?\n?\[mcp_servers\.agentvalet\][\s\S]*?(?=\r?\n\[|\r?\n*$)/,
@@ -1928,9 +2024,9 @@ function stripCodexTomlAt(filePath) {
1928
2024
  }
1929
2025
  }
1930
2026
  function isAgentValetGeneratedDoc(filePath, markers) {
1931
- if (!existsSync7(filePath)) return false;
2027
+ if (!existsSync8(filePath)) return false;
1932
2028
  try {
1933
- const content = readFileSync4(filePath, "utf-8");
2029
+ const content = readFileSync5(filePath, "utf-8");
1934
2030
  const firstLine = content.split(/\r?\n/).find((l) => l.trim().length > 0) ?? "";
1935
2031
  return markers.some((m) => firstLine.startsWith(m));
1936
2032
  } catch {
@@ -1942,13 +2038,13 @@ function cleanLocally() {
1942
2038
  const stripped = [];
1943
2039
  const r = (p) => {
1944
2040
  const abs = resolve3(p);
1945
- if (existsSync7(abs)) {
2041
+ if (existsSync8(abs)) {
1946
2042
  rmSync(abs, { recursive: true, force: true });
1947
2043
  removed.push(abs);
1948
2044
  }
1949
2045
  };
1950
2046
  const rAbs = (abs) => {
1951
- if (existsSync7(abs)) {
2047
+ if (existsSync8(abs)) {
1952
2048
  rmSync(abs, { recursive: true, force: true });
1953
2049
  removed.push(abs);
1954
2050
  }
@@ -1971,12 +2067,12 @@ function cleanLocally() {
1971
2067
  if (stripMcpJson(resolve3(".cursor/mcp.json"))) stripped.push(".cursor/mcp.json");
1972
2068
  if (stripMcpJson(resolve3(".factory/mcp.json"), { wrapped: false })) stripped.push(".factory/mcp.json");
1973
2069
  const home = homedir();
1974
- if (stripMcpJson(join(home, ".claude.json"))) stripped.push("~/.claude.json");
1975
- const claudeDesktopConfig = process.platform === "darwin" ? join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : process.platform === "win32" ? join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json") : join(home, ".config", "Claude", "claude_desktop_config.json");
2070
+ if (stripMcpJson(join3(home, ".claude.json"))) stripped.push("~/.claude.json");
2071
+ const claudeDesktopConfig = process.platform === "darwin" ? join3(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : process.platform === "win32" ? join3(process.env.APPDATA ?? join3(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json") : join3(home, ".config", "Claude", "claude_desktop_config.json");
1976
2072
  if (stripMcpJson(claudeDesktopConfig)) stripped.push("claude_desktop_config.json");
1977
- if (stripMcpJson(join(home, ".cursor", "mcp.json"))) stripped.push("~/.cursor/mcp.json");
1978
- if (stripMcpJson(join(home, ".factory", "mcp.json"), { wrapped: false })) stripped.push("~/.factory/mcp.json");
1979
- if (stripCodexTomlAt(join(home, ".codex", "config.toml"))) stripped.push("~/.codex/config.toml");
2073
+ if (stripMcpJson(join3(home, ".cursor", "mcp.json"))) stripped.push("~/.cursor/mcp.json");
2074
+ if (stripMcpJson(join3(home, ".factory", "mcp.json"), { wrapped: false })) stripped.push("~/.factory/mcp.json");
2075
+ if (stripCodexTomlAt(join3(home, ".codex", "config.toml"))) stripped.push("~/.codex/config.toml");
1980
2076
  if (stripCodexTomlAt(resolve3(".codex/config.toml"))) stripped.push(".codex/config.toml");
1981
2077
  stripClaudeMd();
1982
2078
  stripSettingsJson();
@@ -2081,7 +2177,7 @@ function uninstallCommand() {
2081
2177
  }
2082
2178
  let privateKeyPem;
2083
2179
  try {
2084
- privateKeyPem = readFileSync4(creds.keyPath, "utf-8").trim();
2180
+ privateKeyPem = readFileSync5(creds.keyPath, "utf-8").trim();
2085
2181
  } catch {
2086
2182
  }
2087
2183
  if (!privateKeyPem) {
@@ -2198,13 +2294,13 @@ function revokeCommand() {
2198
2294
  import { Command as Command4 } from "commander";
2199
2295
  import chalk11 from "chalk";
2200
2296
  import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile8 } from "fs/promises";
2201
- import { dirname as dirname2 } from "path";
2297
+ import { dirname as dirname3 } from "path";
2202
2298
 
2203
2299
  // src/lib/claude-config.ts
2204
2300
  import { homedir as homedir2 } from "os";
2205
- import { join as join2 } from "path";
2301
+ import { join as join4 } from "path";
2206
2302
  import { readFile as readFile3, writeFile as writeFile6, mkdir as mkdir4, access } from "fs/promises";
2207
- var CLAUDE_CONFIG_PATH = join2(homedir2(), ".claude", "config");
2303
+ var CLAUDE_CONFIG_PATH = join4(homedir2(), ".claude", "config");
2208
2304
  async function writeKeyToClaudeConfig(agentId, privateKeyPem) {
2209
2305
  let config = {};
2210
2306
  try {
@@ -2215,7 +2311,7 @@ async function writeKeyToClaudeConfig(agentId, privateKeyPem) {
2215
2311
  }
2216
2312
  config.agent_keys = config.agent_keys ?? {};
2217
2313
  config.agent_keys[agentId] = Buffer.from(privateKeyPem).toString("base64");
2218
- await mkdir4(join2(homedir2(), ".claude"), { recursive: true });
2314
+ await mkdir4(join4(homedir2(), ".claude"), { recursive: true });
2219
2315
  await writeFile6(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
2220
2316
  }
2221
2317
 
@@ -2250,7 +2346,7 @@ function injectCommand() {
2250
2346
  "--target <target>",
2251
2347
  "Where to write the key: agentvalet (default \u2014 modern store + MCP configs), env-local (.env.local), claude-legacy (~/.claude/config, deprecated)",
2252
2348
  "agentvalet"
2253
- ).option("--no-frameworks", "Skip IDE/framework configuration prompts (only with --target agentvalet)").option("--project", "Write project-scoped framework configs instead of user-scoped").action(async (opts) => {
2349
+ ).option("--no-frameworks", "Skip IDE/framework configuration prompts (only with --target agentvalet)").option("--project", "Write project-scoped framework configs instead of user-scoped").option("--force", "Replace an existing agent key (a timestamped backup is written first)").action(async (opts) => {
2254
2350
  let privateKeyPem;
2255
2351
  try {
2256
2352
  privateKeyPem = await readFile5(opts.keyFile, "utf-8");
@@ -2270,7 +2366,8 @@ function injectCommand() {
2270
2366
  console.error(chalk11.dim(" Find it at https://app.agentvalet.ai or in your existing ~/.agentvalet/config.json"));
2271
2367
  process.exit(1);
2272
2368
  }
2273
- await mkdir5(dirname2(AGENT_KEY_PATH), { recursive: true });
2369
+ await mkdir5(dirname3(AGENT_KEY_PATH), { recursive: true });
2370
+ assertSafeToWriteKey(AGENT_KEY_PATH, opts.agentId, { force: opts.force });
2274
2371
  await writeFile8(AGENT_KEY_PATH, privateKeyPem, { mode: 384 });
2275
2372
  saveConfig({
2276
2373
  agentId: opts.agentId,
@@ -2325,7 +2422,7 @@ function injectCommand() {
2325
2422
  process.exit(1);
2326
2423
  }
2327
2424
  } catch (err) {
2328
- console.error(chalk11.red(String(err)));
2425
+ console.error(chalk11.red(err instanceof KeyClobberError ? err.message : String(err)));
2329
2426
  process.exit(1);
2330
2427
  }
2331
2428
  console.log(chalk11.green("\n Done. Your agent is ready to use."));
@@ -2368,7 +2465,7 @@ async function bindInstallToken(proxyUrl, bootstrapToken) {
2368
2465
  return { ...body, privateKeyPem: privateKey };
2369
2466
  }
2370
2467
  function installCommand() {
2371
- return new Command5("install").description("Install an agent on this machine using a one-time install token (generates the keypair locally and binds it)").argument("<token>", "install token (itok_...) from the dashboard").option("--no-frameworks", "skip IDE/framework configuration prompts").option("--project", "write project-scoped configs (.mcp.json, .cursor/mcp.json) instead of user-scoped (~/.claude.json, ~/.cursor/mcp.json)").action(async (token, opts) => {
2468
+ return new Command5("install").description("Install an agent on this machine using a one-time install token (generates the keypair locally and binds it)").argument("<token>", "install token (itok_...) from the dashboard").option("--no-frameworks", "skip IDE/framework configuration prompts").option("--project", "write project-scoped configs (.mcp.json, .cursor/mcp.json) instead of user-scoped (~/.claude.json, ~/.cursor/mcp.json)").option("--force", "replace an existing agent key (a timestamped backup is written first)").action(async (token, opts) => {
2372
2469
  const scope = opts.project ? "project" : "user";
2373
2470
  if (!token.startsWith("itok_")) {
2374
2471
  console.error(chalk12.red('Invalid install token. Tokens start with "itok_".'));
@@ -2394,8 +2491,14 @@ function installCommand() {
2394
2491
  const agent_name = claim.agent_id;
2395
2492
  try {
2396
2493
  await mkdir6(AGENT_DIR, { recursive: true });
2494
+ assertSafeToWriteKey(AGENT_KEY_PATH, agent_id, { force: opts.force });
2397
2495
  await writeFile9(AGENT_KEY_PATH, private_key_pem, { mode: 384 });
2398
2496
  } catch (err) {
2497
+ if (err instanceof KeyClobberError) {
2498
+ console.error(chalk12.red(`
2499
+ \u2717 ${err.message}`));
2500
+ process.exit(3);
2501
+ }
2399
2502
  console.error(chalk12.red(`
2400
2503
  \u2717 Failed to write key: ${err instanceof Error ? err.message : err}`));
2401
2504
  process.exit(3);
@@ -2460,6 +2563,7 @@ function installCommand() {
2460
2563
  detected
2461
2564
  });
2462
2565
  }
2566
+ await writeGovernanceInstructions(agent_id, owner_id);
2463
2567
  ensureMcpServer();
2464
2568
  }
2465
2569
  }
@@ -2530,14 +2634,14 @@ function installCommand() {
2530
2634
  // src/commands/claim.ts
2531
2635
  import { Command as Command6 } from "commander";
2532
2636
  import { generateKeyPairSync as generateKeyPairSync2 } from "crypto";
2533
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, existsSync as existsSync8 } from "fs";
2637
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, existsSync as existsSync9 } from "fs";
2534
2638
  import { homedir as homedir3 } from "os";
2535
- import { join as join3 } from "path";
2639
+ import { join as join5 } from "path";
2536
2640
  import chalk13 from "chalk";
2537
2641
  var DEFAULT_PROXY_URL = "https://api.agentvalet.ai";
2538
2642
  function resolveAgentDir(label) {
2539
- const root = join3(homedir3(), ".agentvalet");
2540
- return label ? join3(root, label) : root;
2643
+ const root = join5(homedir3(), ".agentvalet");
2644
+ return label ? join5(root, label) : root;
2541
2645
  }
2542
2646
  function sanitiseLabel(label) {
2543
2647
  return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -2581,9 +2685,9 @@ function claimCommand() {
2581
2685
  process.exit(3);
2582
2686
  }
2583
2687
  const agentDir = resolveAgentDir(label);
2584
- const keyPath = join3(agentDir, "agent.key");
2585
- const identityPath = join3(agentDir, "agent.json");
2586
- if (existsSync8(keyPath)) {
2688
+ const keyPath = join5(agentDir, "agent.key");
2689
+ const identityPath = join5(agentDir, "agent.json");
2690
+ if (existsSync9(keyPath)) {
2587
2691
  const msg = label ? `An agent already exists at ${keyPath}. Pick a different --label or remove the directory first.` : `A default agent already exists at ${keyPath}. Pass --label <name> to claim alongside it without overwriting.`;
2588
2692
  console.error(isJson ? JSON.stringify({ error: msg }) : chalk13.red(msg));
2589
2693
  process.exit(3);
@@ -2675,7 +2779,7 @@ function collectScope(value, previous) {
2675
2779
  return [...previous, value];
2676
2780
  }
2677
2781
  var program = new Command7();
2678
- program.name("agentvalet-register").description("Self-register an AI agent with AgentValet").version(version).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("--json", "Machine-readable JSON output").option("--no-wait", "Register only, do not wait for approval").option("--timeout <seconds>", "Approval wait timeout in seconds", "300").option("--project", "Write project-scoped MCP configs (.mcp.json, .cursor/mcp.json) instead of user-scoped (~/.claude.json, ~/.cursor/mcp.json)").action(async (opts) => {
2782
+ program.name("agentvalet-register").description("Self-register an AI agent with AgentValet").version(version).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("--json", "Machine-readable JSON output").option("--no-wait", "Register only, do not wait for approval").option("--timeout <seconds>", "Approval wait timeout in seconds", "300").option("--project", "Write project-scoped MCP configs (.mcp.json, .cursor/mcp.json) instead of user-scoped (~/.claude.json, ~/.cursor/mcp.json)").option("--force", "Replace an existing agent's key (a timestamped backup is written first)").action(async (opts) => {
2679
2783
  const isJson = !!opts.json;
2680
2784
  let ownerId = opts.owner ?? process.env.AGENTVALET_OWNER ?? "";
2681
2785
  let agentName = opts.name ?? "";
@@ -2791,10 +2895,15 @@ Approve at: https://app.agentvalet.ai/agents
2791
2895
  agentId,
2792
2896
  ownerId: resolvedOwnerId,
2793
2897
  privateKey,
2794
- apiUrl: opts.apiUrl
2898
+ apiUrl: opts.apiUrl,
2899
+ force: !!opts.force
2795
2900
  }));
2796
2901
  } catch (err) {
2797
2902
  const msg = err instanceof Error ? err.message : String(err);
2903
+ if (err instanceof KeyClobberError) {
2904
+ console.error(isJson ? JSON.stringify({ error: msg, existing_agent_id: err.existingAgentId }) : chalk14.red(msg));
2905
+ process.exit(3);
2906
+ }
2798
2907
  console.error(isJson ? JSON.stringify({ error: msg }) : chalk14.red(`Failed to save output: ${msg}`));
2799
2908
  process.exit(3);
2800
2909
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvalet/register",
3
- "version": "1.1.1",
3
+ "version": "1.4.0",
4
4
  "description": "Self-register AI agents with AgentValet",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,13 @@
10
10
  "dist",
11
11
  "README.md"
12
12
  ],
13
+ "scripts": {
14
+ "build": "tsup src/index.ts --format esm --dts --clean --shims",
15
+ "dev": "tsx src/index.ts",
16
+ "test": "vitest run",
17
+ "typecheck": "tsc --noEmit",
18
+ "prepublishOnly": "node ../../scripts/no-secrets.mjs && pnpm build && node ../../scripts/no-workspace-protocol.mjs"
19
+ },
13
20
  "dependencies": {
14
21
  "@modelcontextprotocol/sdk": "^1.29.0",
15
22
  "@octokit/auth-oauth-device": "^7.0.0",
@@ -22,22 +29,23 @@
22
29
  "undici": "^6.0.0"
23
30
  },
24
31
  "devDependencies": {
32
+ "@agentvalet/mcp-server": "workspace:^",
25
33
  "@types/node": "^20.0.0",
26
34
  "@types/prompts": "^2.4.0",
27
35
  "tsup": "^8.0.0",
28
36
  "tsx": "^4.0.0",
29
37
  "typescript": "^5.0.0",
30
- "vitest": "^1.5.0",
31
- "@agentvalet/mcp-server": "^1.4.1"
38
+ "vitest": "^1.5.0"
32
39
  },
33
40
  "engines": {
34
41
  "node": ">=18"
35
42
  },
36
43
  "repository": {
37
44
  "type": "git",
38
- "url": "git+https://github.com/agentvalet/agentvalet.git"
45
+ "url": "git+https://github.com/AgentValet/AgentValet.git",
46
+ "directory": "packages/cli"
39
47
  },
40
- "homepage": "https://agentvalet.ai",
48
+ "homepage": "https://github.com/AgentValet/AgentValet#readme",
41
49
  "bugs": {
42
50
  "url": "https://github.com/agentvalet/agentvalet/issues"
43
51
  },
@@ -53,11 +61,5 @@
53
61
  "valet",
54
62
  "agentvalet"
55
63
  ],
56
- "license": "MIT",
57
- "scripts": {
58
- "build": "tsup src/index.ts --format esm --dts --clean --shims",
59
- "dev": "tsx src/index.ts",
60
- "test": "vitest run",
61
- "typecheck": "tsc --noEmit"
62
- }
63
- }
64
+ "license": "MIT"
65
+ }