@agentvalet/register 1.3.0 → 1.5.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 +186 -103
  3. package/package.json +1 -1
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, join, 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
  }
@@ -264,7 +328,7 @@ Do not call platform APIs directly. Tell the user:
264
328
  }
265
329
  function writeSkillMd() {
266
330
  const dest = path.join(process.cwd(), "SKILL.md");
267
- if (existsSync(dest)) {
331
+ if (existsSync2(dest)) {
268
332
  console.log(chalk2.dim(" SKILL.md already exists \u2014 skipping"));
269
333
  return;
270
334
  }
@@ -302,7 +366,7 @@ Dashboard: https://app.agentvalet.ai
302
366
  }
303
367
  function writeQuickstartDoc(opts) {
304
368
  const dest = path.join(process.cwd(), "AGENTVALET.md");
305
- if (existsSync(dest)) {
369
+ if (existsSync2(dest)) {
306
370
  console.log(chalk2.dim(" AGENTVALET.md already exists \u2014 skipping"));
307
371
  return;
308
372
  }
@@ -385,9 +449,9 @@ AGENT_PRIVATE_KEY_PATH=${keyPath}
385
449
  }
386
450
  function writeFactoryDroidDroid() {
387
451
  const droidsDir = path.join(process.cwd(), ".factory", "droids");
388
- if (!existsSync(droidsDir)) mkdirSync(droidsDir, { recursive: true });
452
+ if (!existsSync2(droidsDir)) mkdirSync(droidsDir, { recursive: true });
389
453
  const dest = path.join(droidsDir, "agentvalet.yaml");
390
- if (existsSync(dest)) {
454
+ if (existsSync2(dest)) {
391
455
  console.log(chalk2.dim(" Factory Droid: .factory/droids/agentvalet.yaml already exists"));
392
456
  return;
393
457
  }
@@ -457,9 +521,9 @@ instructions: |
457
521
  }
458
522
  function writeFactoryDroidCommand() {
459
523
  const commandsDir = path.join(process.cwd(), ".factory", "commands");
460
- if (!existsSync(commandsDir)) mkdirSync(commandsDir, { recursive: true });
524
+ if (!existsSync2(commandsDir)) mkdirSync(commandsDir, { recursive: true });
461
525
  const statusDest = path.join(commandsDir, "av-status.md");
462
- if (!existsSync(statusDest)) {
526
+ if (!existsSync2(statusDest)) {
463
527
  const statusContent = `# /av-status
464
528
 
465
529
  Check your AgentValet agent status and approved platform connections.
@@ -482,7 +546,7 @@ Check your AgentValet agent status and approved platform connections.
482
546
  console.log(chalk2.dim(" Factory Droid: .factory/commands/av-status.md already exists"));
483
547
  }
484
548
  const registerDest = path.join(commandsDir, "av-register.md");
485
- if (!existsSync(registerDest)) {
549
+ if (!existsSync2(registerDest)) {
486
550
  const registerContent = `# /av-register
487
551
 
488
552
  Register or re-register this project with AgentValet.
@@ -509,9 +573,9 @@ After running, approve the agent at: https://app.agentvalet.ai/agents
509
573
  }
510
574
  function writeFactoryDroidReliabilityDroid() {
511
575
  const droidsDir = path.join(process.cwd(), ".factory", "droids");
512
- if (!existsSync(droidsDir)) mkdirSync(droidsDir, { recursive: true });
576
+ if (!existsSync2(droidsDir)) mkdirSync(droidsDir, { recursive: true });
513
577
  const dest = path.join(droidsDir, "agentvalet-reliability.yaml");
514
- if (existsSync(dest)) return;
578
+ if (existsSync2(dest)) return;
515
579
  const content = `# AgentValet Reliability Droid
516
580
  # Monitors agent call patterns and surfaces governance issues
517
581
  # Commit this file to your repo
@@ -597,8 +661,15 @@ function generatePermissionsFile(agentId, ownerId, permissions) {
597
661
  const gated = new Set(p.approvalScopes ?? []);
598
662
  const scopeList = p.scopes.map((s) => ` - \`${s}\`${gated.has(s) ? " \u2014 requires human approval" : ""}`).join("\n");
599
663
  const approvalNote = p.requireApproval ? "\n > Requires human approval before executing.\n" : "";
664
+ const vendorBits = [
665
+ p.serverName ? `reports itself as \`${p.serverName}\`` : null,
666
+ p.serverHost ? `served from \`${p.serverHost}\`` : null
667
+ ].filter(Boolean);
668
+ const vendorNote = vendorBits.length ? `
669
+ _MCP server \u2014 ${vendorBits.join(", ")}._
670
+ ` : "";
600
671
  return `### ${p.platformName} (\`${p.platformId}\`)
601
-
672
+ ${vendorNote}
602
673
  Approved scopes:
603
674
  ${scopeList}
604
675
  ${approvalNote}`;
@@ -677,7 +748,7 @@ To refresh this file: \`npx @agentvalet/register refresh\`
677
748
  }
678
749
 
679
750
  // src/lib/require-agent.ts
680
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
751
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
681
752
  import { resolve } from "path";
682
753
  import chalk3 from "chalk";
683
754
  var NO_AGENT_MSG = chalk3.red("\n \u2717 No agent registered.\n") + chalk3.dim(
@@ -710,9 +781,9 @@ function tryResolveAgent(envFile = ".env.agentvalet") {
710
781
  if (!keyPath) keyPath = AGENT_KEY_PATH;
711
782
  if (!agentId || !ownerId) {
712
783
  const envPath = resolve(envFile);
713
- if (existsSync2(envPath)) {
784
+ if (existsSync3(envPath)) {
714
785
  try {
715
- const vars = parseEnvFile(readFileSync2(envPath, "utf-8"));
786
+ const vars = parseEnvFile(readFileSync3(envPath, "utf-8"));
716
787
  agentId = agentId ?? vars["AGENTVALET_AGENT_ID"];
717
788
  ownerId = ownerId ?? vars["AGENTVALET_OWNER_ID"];
718
789
  apiUrl = vars["AGENTVALET_API_URL"] ?? apiUrl;
@@ -726,12 +797,12 @@ function tryResolveAgent(envFile = ".env.agentvalet") {
726
797
  if (process.env["AGENTVALET_API_URL"]) apiUrl = process.env["AGENTVALET_API_URL"];
727
798
  if (process.env["AGENTVALET_PRIVATE_KEY_PATH"]) keyPath = process.env["AGENTVALET_PRIVATE_KEY_PATH"];
728
799
  if (!agentId || !ownerId || !keyPath) return null;
729
- if (!existsSync2(resolve(keyPath))) return null;
800
+ if (!existsSync3(resolve(keyPath))) return null;
730
801
  return { agentId, ownerId, keyPath, apiUrl };
731
802
  }
732
803
 
733
804
  // src/config/frameworks.ts
734
- import { existsSync as existsSync3 } from "fs";
805
+ import { existsSync as existsSync4 } from "fs";
735
806
  import { execSync as execSync2 } from "child_process";
736
807
  import os from "os";
737
808
  import path2 from "path";
@@ -749,27 +820,27 @@ function codexDesktopInstalled() {
749
820
  const home = os.homedir();
750
821
  const platform = process.platform;
751
822
  if (platform === "darwin") {
752
- return existsSync3("/Applications/Codex.app") || existsSync3(path2.join(home, "Applications", "Codex.app"));
823
+ return existsSync4("/Applications/Codex.app") || existsSync4(path2.join(home, "Applications", "Codex.app"));
753
824
  }
754
825
  if (platform === "win32") {
755
826
  const localAppData = process.env.LOCALAPPDATA ?? path2.join(home, "AppData", "Local");
756
827
  const programFiles = process.env["ProgramFiles"] ?? "C:\\Program Files";
757
- return existsSync3(path2.join(localAppData, "Programs", "codex")) || existsSync3(path2.join(localAppData, "Programs", "Codex")) || existsSync3(path2.join(programFiles, "Codex"));
828
+ return existsSync4(path2.join(localAppData, "Programs", "codex")) || existsSync4(path2.join(localAppData, "Programs", "Codex")) || existsSync4(path2.join(programFiles, "Codex"));
758
829
  }
759
- return existsSync3("/usr/share/applications/codex.desktop") || existsSync3(path2.join(home, ".local", "share", "applications", "codex.desktop"));
830
+ return existsSync4("/usr/share/applications/codex.desktop") || existsSync4(path2.join(home, ".local", "share", "applications", "codex.desktop"));
760
831
  }
761
832
  function claudeDesktopInstalled() {
762
833
  const home = os.homedir();
763
834
  const platform = process.platform;
764
835
  if (platform === "darwin") {
765
- return existsSync3("/Applications/Claude.app") || existsSync3(path2.join(home, "Library", "Application Support", "Claude"));
836
+ return existsSync4("/Applications/Claude.app") || existsSync4(path2.join(home, "Library", "Application Support", "Claude"));
766
837
  }
767
838
  if (platform === "win32") {
768
839
  const appData = process.env.APPDATA ?? path2.join(home, "AppData", "Roaming");
769
840
  const localAppData = process.env.LOCALAPPDATA ?? path2.join(home, "AppData", "Local");
770
- return existsSync3(path2.join(appData, "Claude")) || existsSync3(path2.join(localAppData, "AnthropicClaude")) || existsSync3(path2.join(localAppData, "Programs", "claude"));
841
+ return existsSync4(path2.join(appData, "Claude")) || existsSync4(path2.join(localAppData, "AnthropicClaude")) || existsSync4(path2.join(localAppData, "Programs", "claude"));
771
842
  }
772
- return existsSync3(path2.join(home, ".config", "Claude"));
843
+ return existsSync4(path2.join(home, ".config", "Claude"));
773
844
  }
774
845
  function cursorInstalled() {
775
846
  const home = os.homedir();
@@ -777,24 +848,24 @@ function cursorInstalled() {
777
848
  if (platform === "win32") {
778
849
  const appData = process.env.APPDATA ?? path2.join(home, "AppData", "Roaming");
779
850
  const localAppData = process.env.LOCALAPPDATA ?? path2.join(home, "AppData", "Local");
780
- return existsSync3(path2.join(appData, "Cursor")) || existsSync3(path2.join(localAppData, "Programs", "cursor"));
851
+ return existsSync4(path2.join(appData, "Cursor")) || existsSync4(path2.join(localAppData, "Programs", "cursor"));
781
852
  }
782
853
  if (platform === "darwin") {
783
- return existsSync3(path2.join(home, "Library", "Application Support", "Cursor")) || existsSync3("/Applications/Cursor.app");
854
+ return existsSync4(path2.join(home, "Library", "Application Support", "Cursor")) || existsSync4("/Applications/Cursor.app");
784
855
  }
785
- return existsSync3(path2.join(home, ".config", "Cursor")) || existsSync3("/usr/share/applications/cursor.desktop");
856
+ return existsSync4(path2.join(home, ".config", "Cursor")) || existsSync4("/usr/share/applications/cursor.desktop");
786
857
  }
787
858
  function detectFrameworks() {
788
859
  const home = os.homedir();
789
860
  return {
790
- codex: existsSync3(path2.join(home, ".codex")) || tryBinary("codex"),
861
+ codex: existsSync4(path2.join(home, ".codex")) || tryBinary("codex"),
791
862
  codexDesktop: codexDesktopInstalled(),
792
- claudeCode: existsSync3(path2.join(home, ".claude")) || tryBinary("claude"),
863
+ claudeCode: existsSync4(path2.join(home, ".claude")) || tryBinary("claude"),
793
864
  claudeDesktop: claudeDesktopInstalled(),
794
- cursor: existsSync3(path2.join(process.cwd(), ".cursor")) || cursorInstalled(),
865
+ cursor: existsSync4(path2.join(process.cwd(), ".cursor")) || cursorInstalled(),
795
866
  cursorCli: tryBinary("cursor-agent"),
796
- openclaw: existsSync3(path2.join(home, ".openclaw")),
797
- factoryDroid: existsSync3(path2.join(home, ".factory")) || existsSync3(path2.join(process.cwd(), ".factory"))
867
+ openclaw: existsSync4(path2.join(home, ".openclaw")),
868
+ factoryDroid: existsSync4(path2.join(home, ".factory")) || existsSync4(path2.join(process.cwd(), ".factory"))
798
869
  };
799
870
  }
800
871
  function resolveAgentType(detected) {
@@ -862,13 +933,13 @@ var CLAUDE_END = "<!-- agentvalet:end -->";
862
933
  var HOOK_COMMAND = "npx @agentvalet/register refresh --if-stale --quiet 2>/dev/null || true";
863
934
  var HOOK_MARKER = "agentvalet/register refresh";
864
935
  function projectRootFor(outputPath) {
865
- return dirname(dirname(resolve2(outputPath)));
936
+ return dirname2(dirname2(resolve2(outputPath)));
866
937
  }
867
938
  function writePermissionsFromEvent(agentId, ownerId, platforms, outputPath = "./.claude/agentvalet-permissions.md") {
868
939
  const content = generatePermissionsFile(agentId, ownerId, platforms);
869
940
  const resolvedPath = resolve2(outputPath);
870
941
  const projectRoot = projectRootFor(resolvedPath);
871
- mkdirSync2(dirname(resolvedPath), { recursive: true });
942
+ mkdirSync2(dirname2(resolvedPath), { recursive: true });
872
943
  writeFileSync2(resolvedPath, content, "utf-8");
873
944
  embedInClaudeMd(content, projectRoot);
874
945
  ensureHook(projectRoot);
@@ -908,8 +979,8 @@ async function runRefresh(options) {
908
979
  try {
909
980
  const stat = statSync(outputPath);
910
981
  const fileIsFresh = Date.now() - stat.mtimeMs < 36e5;
911
- const claudeMdPath = join(projectRoot, "CLAUDE.md");
912
- const claudeMd = existsSync4(claudeMdPath) ? readFileSync3(claudeMdPath, "utf-8") : "";
982
+ const claudeMdPath = join2(projectRoot, "CLAUDE.md");
983
+ const claudeMd = existsSync5(claudeMdPath) ? readFileSync4(claudeMdPath, "utf-8") : "";
913
984
  const blockStart = claudeMd.indexOf(CLAUDE_START);
914
985
  const blockEnd = claudeMd.indexOf(CLAUDE_END);
915
986
  const blockContent = blockStart !== -1 && blockEnd !== -1 ? claudeMd.slice(blockStart, blockEnd) : "";
@@ -919,14 +990,14 @@ async function runRefresh(options) {
919
990
  }
920
991
  }
921
992
  const creds = tryResolveAgent(options.envFile);
922
- if (!creds || !existsSync4(resolve2(creds.keyPath))) {
993
+ if (!creds || !existsSync5(resolve2(creds.keyPath))) {
923
994
  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");
924
995
  process.exit(1);
925
996
  }
926
997
  const { agentId, ownerId, keyPath, apiUrl } = creds;
927
998
  let privateKeyPem;
928
999
  try {
929
- privateKeyPem = readFileSync3(keyPath, "utf-8");
1000
+ privateKeyPem = readFileSync4(keyPath, "utf-8");
930
1001
  } catch {
931
1002
  throw new Error(`Cannot read private key: ${keyPath}`);
932
1003
  }
@@ -944,17 +1015,17 @@ async function runRefresh(options) {
944
1015
  const json = await res.json();
945
1016
  const platforms = json.data?.platforms ?? [];
946
1017
  const content = generatePermissionsFile(agentId, ownerId, platforms);
947
- mkdirSync2(dirname(outputPath), { recursive: true });
1018
+ mkdirSync2(dirname2(outputPath), { recursive: true });
948
1019
  writeFileSync2(outputPath, content, "utf-8");
949
1020
  embedInClaudeMd(content, projectRoot);
950
1021
  ensureHook(projectRoot);
951
1022
  const detected = detectFrameworks();
952
1023
  const refreshed = [];
953
1024
  if (detected.codex || detected.codexDesktop) {
954
- if (embedPermissionsBlock(join(projectRoot, "AGENTS.md"), content)) refreshed.push("AGENTS.md");
1025
+ if (embedPermissionsBlock(join2(projectRoot, "AGENTS.md"), content)) refreshed.push("AGENTS.md");
955
1026
  }
956
1027
  if (detected.openclaw) {
957
- if (embedPermissionsBlock(join(projectRoot, "SKILL.md"), content)) refreshed.push("SKILL.md");
1028
+ if (embedPermissionsBlock(join2(projectRoot, "SKILL.md"), content)) refreshed.push("SKILL.md");
958
1029
  }
959
1030
  if (!options.quiet) {
960
1031
  const extras = refreshed.length > 0 ? ` (also: ${refreshed.join(", ")})` : "";
@@ -962,9 +1033,9 @@ async function runRefresh(options) {
962
1033
  }
963
1034
  }
964
1035
  function embedPermissionsBlock(filePath, permissionsContent) {
965
- if (!existsSync4(filePath)) return false;
1036
+ if (!existsSync5(filePath)) return false;
966
1037
  try {
967
- const existing = readFileSync3(filePath, "utf-8");
1038
+ const existing = readFileSync4(filePath, "utf-8");
968
1039
  const block = `${PERMISSIONS_START}
969
1040
  ${permissionsContent.trimEnd()}
970
1041
  ${PERMISSIONS_END}`;
@@ -987,18 +1058,18 @@ ${block}
987
1058
  }
988
1059
  }
989
1060
  function embedInClaudeMd(permissionsContent, projectRoot) {
990
- const claudeMdPath = join(projectRoot, "CLAUDE.md");
1061
+ const claudeMdPath = join2(projectRoot, "CLAUDE.md");
991
1062
  const block = `${CLAUDE_START}
992
1063
  ${permissionsContent.trimEnd()}
993
1064
  ${CLAUDE_END}`;
994
- if (!existsSync4(claudeMdPath)) {
1065
+ if (!existsSync5(claudeMdPath)) {
995
1066
  writeFileSync2(claudeMdPath, `# Agent Instructions
996
1067
 
997
1068
  ${block}
998
1069
  `, "utf-8");
999
1070
  return;
1000
1071
  }
1001
- const existing = readFileSync3(claudeMdPath, "utf-8");
1072
+ const existing = readFileSync4(claudeMdPath, "utf-8");
1002
1073
  if (existing.includes(CLAUDE_START)) {
1003
1074
  const replaced = existing.replace(
1004
1075
  new RegExp(`${CLAUDE_START}[\\s\\S]*?${CLAUDE_END}`, "m"),
@@ -1014,11 +1085,11 @@ ${block}
1014
1085
  }
1015
1086
  }
1016
1087
  function ensureHook(projectRoot) {
1017
- const settingsPath = join(projectRoot, ".claude", "settings.local.json");
1088
+ const settingsPath = join2(projectRoot, ".claude", "settings.local.json");
1018
1089
  let settings = {};
1019
- if (existsSync4(settingsPath)) {
1090
+ if (existsSync5(settingsPath)) {
1020
1091
  try {
1021
- settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
1092
+ settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
1022
1093
  } catch {
1023
1094
  return;
1024
1095
  }
@@ -1046,16 +1117,16 @@ function ensureHook(projectRoot) {
1046
1117
  perms.allow = [...allow, ...missing];
1047
1118
  settings.permissions = perms;
1048
1119
  }
1049
- mkdirSync2(dirname(settingsPath), { recursive: true });
1120
+ mkdirSync2(dirname2(settingsPath), { recursive: true });
1050
1121
  writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1051
1122
  ensureMcpJson(projectRoot);
1052
1123
  }
1053
1124
  function ensureMcpJson(projectRoot) {
1054
- const mcpJsonPath = join(projectRoot, ".mcp.json");
1125
+ const mcpJsonPath = join2(projectRoot, ".mcp.json");
1055
1126
  let mcpConfig = {};
1056
- if (existsSync4(mcpJsonPath)) {
1127
+ if (existsSync5(mcpJsonPath)) {
1057
1128
  try {
1058
- mcpConfig = JSON.parse(readFileSync3(mcpJsonPath, "utf-8"));
1129
+ mcpConfig = JSON.parse(readFileSync4(mcpJsonPath, "utf-8"));
1059
1130
  } catch {
1060
1131
  return;
1061
1132
  }
@@ -1254,7 +1325,7 @@ async function writeLocalConfig(config) {
1254
1325
  // src/output/env.ts
1255
1326
  import chalk6 from "chalk";
1256
1327
  import { appendFile as appendFile2, writeFile as writeFile3 } from "fs/promises";
1257
- import { existsSync as existsSync5 } from "fs";
1328
+ import { existsSync as existsSync6 } from "fs";
1258
1329
  import path4 from "path";
1259
1330
  function formatEnvBlock(vars) {
1260
1331
  return Object.entries(vars).map(([key, value]) => {
@@ -1265,7 +1336,7 @@ function formatEnvBlock(vars) {
1265
1336
 
1266
1337
  // src/config/ide.ts
1267
1338
  import { mkdir as mkdir3, writeFile as writeFile4, readFile as readFile2, appendFile as appendFile3, copyFile } from "fs/promises";
1268
- import { existsSync as existsSync6 } from "fs";
1339
+ import { existsSync as existsSync7 } from "fs";
1269
1340
  import path5 from "path";
1270
1341
  import os2 from "os";
1271
1342
  function buildMcpEntry(params) {
@@ -1302,7 +1373,7 @@ function claudeDesktopFriendly() {
1302
1373
  }
1303
1374
  async function mergeJsonFile(filePath, key, value) {
1304
1375
  let existing = {};
1305
- if (existsSync6(filePath)) {
1376
+ if (existsSync7(filePath)) {
1306
1377
  try {
1307
1378
  const text = await readFile2(filePath, "utf8");
1308
1379
  existing = JSON.parse(text);
@@ -1324,8 +1395,8 @@ env = { AGENT_ID = "${params.agentId}", OWNER_ID = "${params.ownerId}", PROXY_UR
1324
1395
  }
1325
1396
  async function appendCodexConfig(configPath) {
1326
1397
  const dir = path5.dirname(configPath);
1327
- if (!existsSync6(dir)) await mkdir3(dir, { recursive: true });
1328
- const existing = existsSync6(configPath) ? await readFile2(configPath, "utf8") : "";
1398
+ if (!existsSync7(dir)) await mkdir3(dir, { recursive: true });
1399
+ const existing = existsSync7(configPath) ? await readFile2(configPath, "utf8") : "";
1329
1400
  if (existing.includes("[mcp_servers.agentvalet]")) return false;
1330
1401
  return true;
1331
1402
  }
@@ -1334,7 +1405,7 @@ async function writeGlobalFrameworkConfigs(params) {
1334
1405
  const codexDir = path5.join(os2.homedir(), ".codex");
1335
1406
  const codexConfigPath = path5.join(codexDir, "config.toml");
1336
1407
  try {
1337
- if (!existsSync6(codexDir)) await mkdir3(codexDir, { recursive: true });
1408
+ if (!existsSync7(codexDir)) await mkdir3(codexDir, { recursive: true });
1338
1409
  if (await appendCodexConfig(codexConfigPath)) {
1339
1410
  await appendFile3(codexConfigPath, buildCodexTomlBlock(params), "utf8");
1340
1411
  written.push("~/.codex/config.toml");
@@ -1359,7 +1430,7 @@ async function writeCodexProjectConfig(params) {
1359
1430
  return written;
1360
1431
  }
1361
1432
  async function stripAgentvaletFromMcpJson(filePath) {
1362
- if (!existsSync6(filePath)) return false;
1433
+ if (!existsSync7(filePath)) return false;
1363
1434
  try {
1364
1435
  const text = await readFile2(filePath, "utf8");
1365
1436
  const config = JSON.parse(text);
@@ -1383,7 +1454,7 @@ async function cleanProjectScopedEntries() {
1383
1454
  cleaned.push(".cursor/mcp.json (project entry removed)");
1384
1455
  }
1385
1456
  const factoryProjectMcp = path5.join(cwd, ".factory/mcp.json");
1386
- if (existsSync6(factoryProjectMcp)) {
1457
+ if (existsSync7(factoryProjectMcp)) {
1387
1458
  try {
1388
1459
  const text = await readFile2(factoryProjectMcp, "utf8");
1389
1460
  const config = JSON.parse(text);
@@ -1474,7 +1545,7 @@ async function writeIdeConfigs(params) {
1474
1545
  }
1475
1546
  async function mergeFactoryDroidMcpJson(filePath, key, value) {
1476
1547
  let existing = {};
1477
- if (existsSync6(filePath)) {
1548
+ if (existsSync7(filePath)) {
1478
1549
  try {
1479
1550
  const text = await readFile2(filePath, "utf8");
1480
1551
  existing = JSON.parse(text);
@@ -1488,7 +1559,7 @@ async function mergeFactoryDroidMcpJson(filePath, key, value) {
1488
1559
  }
1489
1560
  async function updateGitignoreForFactoryDroid() {
1490
1561
  const gitignorePath = path5.join(process.cwd(), ".gitignore");
1491
- if (!existsSync6(gitignorePath)) return;
1562
+ if (!existsSync7(gitignorePath)) return;
1492
1563
  const existing = await readFile2(gitignorePath, "utf8");
1493
1564
  if (existing.includes(".factory/mcp.json")) return;
1494
1565
  const additions = [
@@ -1870,8 +1941,8 @@ import chalk10 from "chalk";
1870
1941
  // src/commands/uninstall.ts
1871
1942
  import { Command as Command2 } from "commander";
1872
1943
  import chalk9 from "chalk";
1873
- import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync3, rmSync } from "fs";
1874
- import { resolve as resolve3, join as join2 } from "path";
1944
+ import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync3, rmSync } from "fs";
1945
+ import { resolve as resolve3, join as join3 } from "path";
1875
1946
  import { homedir } from "os";
1876
1947
  var CLAUDE_START2 = "<!-- agentvalet:start -->";
1877
1948
  var CLAUDE_END2 = "<!-- agentvalet:end -->";
@@ -1879,9 +1950,9 @@ var PERMISSIONS_START2 = "<!-- agentvalet:permissions:start -->";
1879
1950
  var PERMISSIONS_END2 = "<!-- agentvalet:permissions:end -->";
1880
1951
  var HOOK_MARKER2 = "agentvalet/register refresh";
1881
1952
  function stripPermissionsBlock(filePath) {
1882
- if (!existsSync7(filePath)) return false;
1953
+ if (!existsSync8(filePath)) return false;
1883
1954
  try {
1884
- const content = readFileSync4(filePath, "utf-8");
1955
+ const content = readFileSync5(filePath, "utf-8");
1885
1956
  if (!content.includes(PERMISSIONS_START2)) return false;
1886
1957
  const stripped = content.replace(new RegExp(`${PERMISSIONS_START2}[\\s\\S]*?${PERMISSIONS_END2}`, "m"), "").replace(/(\r?\n){3,}/g, "\n\n").trimEnd();
1887
1958
  writeFileSync3(filePath, stripped + "\n", "utf-8");
@@ -1892,8 +1963,8 @@ function stripPermissionsBlock(filePath) {
1892
1963
  }
1893
1964
  function stripClaudeMd() {
1894
1965
  const path8 = resolve3("CLAUDE.md");
1895
- if (!existsSync7(path8)) return;
1896
- let content = readFileSync4(path8, "utf-8");
1966
+ if (!existsSync8(path8)) return;
1967
+ let content = readFileSync5(path8, "utf-8");
1897
1968
  content = content.replace(new RegExp(`${CLAUDE_START2}[\\s\\S]*?${CLAUDE_END2}`, "m"), "");
1898
1969
  content = content.replace(/^@\.claude\/agentvalet-permissions\.md\s*$/m, "");
1899
1970
  content = content.replace(/(\r?\n){3,}/g, "\n\n").trim();
@@ -1901,9 +1972,9 @@ function stripClaudeMd() {
1901
1972
  }
1902
1973
  function stripSettingsJson() {
1903
1974
  const path8 = resolve3(".claude/settings.local.json");
1904
- if (!existsSync7(path8)) return;
1975
+ if (!existsSync8(path8)) return;
1905
1976
  try {
1906
- const settings = JSON.parse(readFileSync4(path8, "utf-8"));
1977
+ const settings = JSON.parse(readFileSync5(path8, "utf-8"));
1907
1978
  const hooks = settings.hooks ?? {};
1908
1979
  const ups = hooks.UserPromptSubmit ?? [];
1909
1980
  hooks.UserPromptSubmit = ups.filter(
@@ -1919,9 +1990,9 @@ function stripSettingsJson() {
1919
1990
  }
1920
1991
  }
1921
1992
  function stripMcpJson(filePath, options = {}) {
1922
- if (!existsSync7(filePath)) return false;
1993
+ if (!existsSync8(filePath)) return false;
1923
1994
  try {
1924
- const config = JSON.parse(readFileSync4(filePath, "utf-8"));
1995
+ const config = JSON.parse(readFileSync5(filePath, "utf-8"));
1925
1996
  let removed = false;
1926
1997
  if (options.wrapped === false) {
1927
1998
  if (config["agentvalet"] !== void 0) {
@@ -1945,9 +2016,9 @@ function stripMcpJson(filePath, options = {}) {
1945
2016
  }
1946
2017
  }
1947
2018
  function stripCodexTomlAt(filePath) {
1948
- if (!existsSync7(filePath)) return false;
2019
+ if (!existsSync8(filePath)) return false;
1949
2020
  try {
1950
- const content = readFileSync4(filePath, "utf-8");
2021
+ const content = readFileSync5(filePath, "utf-8");
1951
2022
  if (!content.includes("[mcp_servers.agentvalet]")) return false;
1952
2023
  const stripped = content.replace(
1953
2024
  /\r?\n?\[mcp_servers\.agentvalet\][\s\S]*?(?=\r?\n\[|\r?\n*$)/,
@@ -1960,9 +2031,9 @@ function stripCodexTomlAt(filePath) {
1960
2031
  }
1961
2032
  }
1962
2033
  function isAgentValetGeneratedDoc(filePath, markers) {
1963
- if (!existsSync7(filePath)) return false;
2034
+ if (!existsSync8(filePath)) return false;
1964
2035
  try {
1965
- const content = readFileSync4(filePath, "utf-8");
2036
+ const content = readFileSync5(filePath, "utf-8");
1966
2037
  const firstLine = content.split(/\r?\n/).find((l) => l.trim().length > 0) ?? "";
1967
2038
  return markers.some((m) => firstLine.startsWith(m));
1968
2039
  } catch {
@@ -1974,13 +2045,13 @@ function cleanLocally() {
1974
2045
  const stripped = [];
1975
2046
  const r = (p) => {
1976
2047
  const abs = resolve3(p);
1977
- if (existsSync7(abs)) {
2048
+ if (existsSync8(abs)) {
1978
2049
  rmSync(abs, { recursive: true, force: true });
1979
2050
  removed.push(abs);
1980
2051
  }
1981
2052
  };
1982
2053
  const rAbs = (abs) => {
1983
- if (existsSync7(abs)) {
2054
+ if (existsSync8(abs)) {
1984
2055
  rmSync(abs, { recursive: true, force: true });
1985
2056
  removed.push(abs);
1986
2057
  }
@@ -2003,12 +2074,12 @@ function cleanLocally() {
2003
2074
  if (stripMcpJson(resolve3(".cursor/mcp.json"))) stripped.push(".cursor/mcp.json");
2004
2075
  if (stripMcpJson(resolve3(".factory/mcp.json"), { wrapped: false })) stripped.push(".factory/mcp.json");
2005
2076
  const home = homedir();
2006
- if (stripMcpJson(join2(home, ".claude.json"))) stripped.push("~/.claude.json");
2007
- const claudeDesktopConfig = process.platform === "darwin" ? join2(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : process.platform === "win32" ? join2(process.env.APPDATA ?? join2(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json") : join2(home, ".config", "Claude", "claude_desktop_config.json");
2077
+ if (stripMcpJson(join3(home, ".claude.json"))) stripped.push("~/.claude.json");
2078
+ 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");
2008
2079
  if (stripMcpJson(claudeDesktopConfig)) stripped.push("claude_desktop_config.json");
2009
- if (stripMcpJson(join2(home, ".cursor", "mcp.json"))) stripped.push("~/.cursor/mcp.json");
2010
- if (stripMcpJson(join2(home, ".factory", "mcp.json"), { wrapped: false })) stripped.push("~/.factory/mcp.json");
2011
- if (stripCodexTomlAt(join2(home, ".codex", "config.toml"))) stripped.push("~/.codex/config.toml");
2080
+ if (stripMcpJson(join3(home, ".cursor", "mcp.json"))) stripped.push("~/.cursor/mcp.json");
2081
+ if (stripMcpJson(join3(home, ".factory", "mcp.json"), { wrapped: false })) stripped.push("~/.factory/mcp.json");
2082
+ if (stripCodexTomlAt(join3(home, ".codex", "config.toml"))) stripped.push("~/.codex/config.toml");
2012
2083
  if (stripCodexTomlAt(resolve3(".codex/config.toml"))) stripped.push(".codex/config.toml");
2013
2084
  stripClaudeMd();
2014
2085
  stripSettingsJson();
@@ -2113,7 +2184,7 @@ function uninstallCommand() {
2113
2184
  }
2114
2185
  let privateKeyPem;
2115
2186
  try {
2116
- privateKeyPem = readFileSync4(creds.keyPath, "utf-8").trim();
2187
+ privateKeyPem = readFileSync5(creds.keyPath, "utf-8").trim();
2117
2188
  } catch {
2118
2189
  }
2119
2190
  if (!privateKeyPem) {
@@ -2230,13 +2301,13 @@ function revokeCommand() {
2230
2301
  import { Command as Command4 } from "commander";
2231
2302
  import chalk11 from "chalk";
2232
2303
  import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile8 } from "fs/promises";
2233
- import { dirname as dirname2 } from "path";
2304
+ import { dirname as dirname3 } from "path";
2234
2305
 
2235
2306
  // src/lib/claude-config.ts
2236
2307
  import { homedir as homedir2 } from "os";
2237
- import { join as join3 } from "path";
2308
+ import { join as join4 } from "path";
2238
2309
  import { readFile as readFile3, writeFile as writeFile6, mkdir as mkdir4, access } from "fs/promises";
2239
- var CLAUDE_CONFIG_PATH = join3(homedir2(), ".claude", "config");
2310
+ var CLAUDE_CONFIG_PATH = join4(homedir2(), ".claude", "config");
2240
2311
  async function writeKeyToClaudeConfig(agentId, privateKeyPem) {
2241
2312
  let config = {};
2242
2313
  try {
@@ -2247,7 +2318,7 @@ async function writeKeyToClaudeConfig(agentId, privateKeyPem) {
2247
2318
  }
2248
2319
  config.agent_keys = config.agent_keys ?? {};
2249
2320
  config.agent_keys[agentId] = Buffer.from(privateKeyPem).toString("base64");
2250
- await mkdir4(join3(homedir2(), ".claude"), { recursive: true });
2321
+ await mkdir4(join4(homedir2(), ".claude"), { recursive: true });
2251
2322
  await writeFile6(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
2252
2323
  }
2253
2324
 
@@ -2282,7 +2353,7 @@ function injectCommand() {
2282
2353
  "--target <target>",
2283
2354
  "Where to write the key: agentvalet (default \u2014 modern store + MCP configs), env-local (.env.local), claude-legacy (~/.claude/config, deprecated)",
2284
2355
  "agentvalet"
2285
- ).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) => {
2356
+ ).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) => {
2286
2357
  let privateKeyPem;
2287
2358
  try {
2288
2359
  privateKeyPem = await readFile5(opts.keyFile, "utf-8");
@@ -2302,7 +2373,8 @@ function injectCommand() {
2302
2373
  console.error(chalk11.dim(" Find it at https://app.agentvalet.ai or in your existing ~/.agentvalet/config.json"));
2303
2374
  process.exit(1);
2304
2375
  }
2305
- await mkdir5(dirname2(AGENT_KEY_PATH), { recursive: true });
2376
+ await mkdir5(dirname3(AGENT_KEY_PATH), { recursive: true });
2377
+ assertSafeToWriteKey(AGENT_KEY_PATH, opts.agentId, { force: opts.force });
2306
2378
  await writeFile8(AGENT_KEY_PATH, privateKeyPem, { mode: 384 });
2307
2379
  saveConfig({
2308
2380
  agentId: opts.agentId,
@@ -2357,7 +2429,7 @@ function injectCommand() {
2357
2429
  process.exit(1);
2358
2430
  }
2359
2431
  } catch (err) {
2360
- console.error(chalk11.red(String(err)));
2432
+ console.error(chalk11.red(err instanceof KeyClobberError ? err.message : String(err)));
2361
2433
  process.exit(1);
2362
2434
  }
2363
2435
  console.log(chalk11.green("\n Done. Your agent is ready to use."));
@@ -2400,7 +2472,7 @@ async function bindInstallToken(proxyUrl, bootstrapToken) {
2400
2472
  return { ...body, privateKeyPem: privateKey };
2401
2473
  }
2402
2474
  function installCommand() {
2403
- 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) => {
2475
+ 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) => {
2404
2476
  const scope = opts.project ? "project" : "user";
2405
2477
  if (!token.startsWith("itok_")) {
2406
2478
  console.error(chalk12.red('Invalid install token. Tokens start with "itok_".'));
@@ -2426,8 +2498,14 @@ function installCommand() {
2426
2498
  const agent_name = claim.agent_id;
2427
2499
  try {
2428
2500
  await mkdir6(AGENT_DIR, { recursive: true });
2501
+ assertSafeToWriteKey(AGENT_KEY_PATH, agent_id, { force: opts.force });
2429
2502
  await writeFile9(AGENT_KEY_PATH, private_key_pem, { mode: 384 });
2430
2503
  } catch (err) {
2504
+ if (err instanceof KeyClobberError) {
2505
+ console.error(chalk12.red(`
2506
+ \u2717 ${err.message}`));
2507
+ process.exit(3);
2508
+ }
2431
2509
  console.error(chalk12.red(`
2432
2510
  \u2717 Failed to write key: ${err instanceof Error ? err.message : err}`));
2433
2511
  process.exit(3);
@@ -2563,14 +2641,14 @@ function installCommand() {
2563
2641
  // src/commands/claim.ts
2564
2642
  import { Command as Command6 } from "commander";
2565
2643
  import { generateKeyPairSync as generateKeyPairSync2 } from "crypto";
2566
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, existsSync as existsSync8 } from "fs";
2644
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, existsSync as existsSync9 } from "fs";
2567
2645
  import { homedir as homedir3 } from "os";
2568
- import { join as join4 } from "path";
2646
+ import { join as join5 } from "path";
2569
2647
  import chalk13 from "chalk";
2570
2648
  var DEFAULT_PROXY_URL = "https://api.agentvalet.ai";
2571
2649
  function resolveAgentDir(label) {
2572
- const root = join4(homedir3(), ".agentvalet");
2573
- return label ? join4(root, label) : root;
2650
+ const root = join5(homedir3(), ".agentvalet");
2651
+ return label ? join5(root, label) : root;
2574
2652
  }
2575
2653
  function sanitiseLabel(label) {
2576
2654
  return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -2614,9 +2692,9 @@ function claimCommand() {
2614
2692
  process.exit(3);
2615
2693
  }
2616
2694
  const agentDir = resolveAgentDir(label);
2617
- const keyPath = join4(agentDir, "agent.key");
2618
- const identityPath = join4(agentDir, "agent.json");
2619
- if (existsSync8(keyPath)) {
2695
+ const keyPath = join5(agentDir, "agent.key");
2696
+ const identityPath = join5(agentDir, "agent.json");
2697
+ if (existsSync9(keyPath)) {
2620
2698
  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.`;
2621
2699
  console.error(isJson ? JSON.stringify({ error: msg }) : chalk13.red(msg));
2622
2700
  process.exit(3);
@@ -2708,7 +2786,7 @@ function collectScope(value, previous) {
2708
2786
  return [...previous, value];
2709
2787
  }
2710
2788
  var program = new Command7();
2711
- 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) => {
2789
+ 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) => {
2712
2790
  const isJson = !!opts.json;
2713
2791
  let ownerId = opts.owner ?? process.env.AGENTVALET_OWNER ?? "";
2714
2792
  let agentName = opts.name ?? "";
@@ -2824,10 +2902,15 @@ Approve at: https://app.agentvalet.ai/agents
2824
2902
  agentId,
2825
2903
  ownerId: resolvedOwnerId,
2826
2904
  privateKey,
2827
- apiUrl: opts.apiUrl
2905
+ apiUrl: opts.apiUrl,
2906
+ force: !!opts.force
2828
2907
  }));
2829
2908
  } catch (err) {
2830
2909
  const msg = err instanceof Error ? err.message : String(err);
2910
+ if (err instanceof KeyClobberError) {
2911
+ console.error(isJson ? JSON.stringify({ error: msg, existing_agent_id: err.existingAgentId }) : chalk14.red(msg));
2912
+ process.exit(3);
2913
+ }
2831
2914
  console.error(isJson ? JSON.stringify({ error: msg }) : chalk14.red(`Failed to save output: ${msg}`));
2832
2915
  process.exit(3);
2833
2916
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvalet/register",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Self-register AI agents with AgentValet",
5
5
  "type": "module",
6
6
  "bin": {