@agentvalet/register 1.3.0 → 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 +178 -102
  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
@@ -677,7 +741,7 @@ To refresh this file: \`npx @agentvalet/register refresh\`
677
741
  }
678
742
 
679
743
  // src/lib/require-agent.ts
680
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
744
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
681
745
  import { resolve } from "path";
682
746
  import chalk3 from "chalk";
683
747
  var NO_AGENT_MSG = chalk3.red("\n \u2717 No agent registered.\n") + chalk3.dim(
@@ -710,9 +774,9 @@ function tryResolveAgent(envFile = ".env.agentvalet") {
710
774
  if (!keyPath) keyPath = AGENT_KEY_PATH;
711
775
  if (!agentId || !ownerId) {
712
776
  const envPath = resolve(envFile);
713
- if (existsSync2(envPath)) {
777
+ if (existsSync3(envPath)) {
714
778
  try {
715
- const vars = parseEnvFile(readFileSync2(envPath, "utf-8"));
779
+ const vars = parseEnvFile(readFileSync3(envPath, "utf-8"));
716
780
  agentId = agentId ?? vars["AGENTVALET_AGENT_ID"];
717
781
  ownerId = ownerId ?? vars["AGENTVALET_OWNER_ID"];
718
782
  apiUrl = vars["AGENTVALET_API_URL"] ?? apiUrl;
@@ -726,12 +790,12 @@ function tryResolveAgent(envFile = ".env.agentvalet") {
726
790
  if (process.env["AGENTVALET_API_URL"]) apiUrl = process.env["AGENTVALET_API_URL"];
727
791
  if (process.env["AGENTVALET_PRIVATE_KEY_PATH"]) keyPath = process.env["AGENTVALET_PRIVATE_KEY_PATH"];
728
792
  if (!agentId || !ownerId || !keyPath) return null;
729
- if (!existsSync2(resolve(keyPath))) return null;
793
+ if (!existsSync3(resolve(keyPath))) return null;
730
794
  return { agentId, ownerId, keyPath, apiUrl };
731
795
  }
732
796
 
733
797
  // src/config/frameworks.ts
734
- import { existsSync as existsSync3 } from "fs";
798
+ import { existsSync as existsSync4 } from "fs";
735
799
  import { execSync as execSync2 } from "child_process";
736
800
  import os from "os";
737
801
  import path2 from "path";
@@ -749,27 +813,27 @@ function codexDesktopInstalled() {
749
813
  const home = os.homedir();
750
814
  const platform = process.platform;
751
815
  if (platform === "darwin") {
752
- 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"));
753
817
  }
754
818
  if (platform === "win32") {
755
819
  const localAppData = process.env.LOCALAPPDATA ?? path2.join(home, "AppData", "Local");
756
820
  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"));
821
+ return existsSync4(path2.join(localAppData, "Programs", "codex")) || existsSync4(path2.join(localAppData, "Programs", "Codex")) || existsSync4(path2.join(programFiles, "Codex"));
758
822
  }
759
- 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"));
760
824
  }
761
825
  function claudeDesktopInstalled() {
762
826
  const home = os.homedir();
763
827
  const platform = process.platform;
764
828
  if (platform === "darwin") {
765
- 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"));
766
830
  }
767
831
  if (platform === "win32") {
768
832
  const appData = process.env.APPDATA ?? path2.join(home, "AppData", "Roaming");
769
833
  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"));
834
+ return existsSync4(path2.join(appData, "Claude")) || existsSync4(path2.join(localAppData, "AnthropicClaude")) || existsSync4(path2.join(localAppData, "Programs", "claude"));
771
835
  }
772
- return existsSync3(path2.join(home, ".config", "Claude"));
836
+ return existsSync4(path2.join(home, ".config", "Claude"));
773
837
  }
774
838
  function cursorInstalled() {
775
839
  const home = os.homedir();
@@ -777,24 +841,24 @@ function cursorInstalled() {
777
841
  if (platform === "win32") {
778
842
  const appData = process.env.APPDATA ?? path2.join(home, "AppData", "Roaming");
779
843
  const localAppData = process.env.LOCALAPPDATA ?? path2.join(home, "AppData", "Local");
780
- 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"));
781
845
  }
782
846
  if (platform === "darwin") {
783
- 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");
784
848
  }
785
- 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");
786
850
  }
787
851
  function detectFrameworks() {
788
852
  const home = os.homedir();
789
853
  return {
790
- codex: existsSync3(path2.join(home, ".codex")) || tryBinary("codex"),
854
+ codex: existsSync4(path2.join(home, ".codex")) || tryBinary("codex"),
791
855
  codexDesktop: codexDesktopInstalled(),
792
- claudeCode: existsSync3(path2.join(home, ".claude")) || tryBinary("claude"),
856
+ claudeCode: existsSync4(path2.join(home, ".claude")) || tryBinary("claude"),
793
857
  claudeDesktop: claudeDesktopInstalled(),
794
- cursor: existsSync3(path2.join(process.cwd(), ".cursor")) || cursorInstalled(),
858
+ cursor: existsSync4(path2.join(process.cwd(), ".cursor")) || cursorInstalled(),
795
859
  cursorCli: tryBinary("cursor-agent"),
796
- openclaw: existsSync3(path2.join(home, ".openclaw")),
797
- 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"))
798
862
  };
799
863
  }
800
864
  function resolveAgentType(detected) {
@@ -862,13 +926,13 @@ var CLAUDE_END = "<!-- agentvalet:end -->";
862
926
  var HOOK_COMMAND = "npx @agentvalet/register refresh --if-stale --quiet 2>/dev/null || true";
863
927
  var HOOK_MARKER = "agentvalet/register refresh";
864
928
  function projectRootFor(outputPath) {
865
- return dirname(dirname(resolve2(outputPath)));
929
+ return dirname2(dirname2(resolve2(outputPath)));
866
930
  }
867
931
  function writePermissionsFromEvent(agentId, ownerId, platforms, outputPath = "./.claude/agentvalet-permissions.md") {
868
932
  const content = generatePermissionsFile(agentId, ownerId, platforms);
869
933
  const resolvedPath = resolve2(outputPath);
870
934
  const projectRoot = projectRootFor(resolvedPath);
871
- mkdirSync2(dirname(resolvedPath), { recursive: true });
935
+ mkdirSync2(dirname2(resolvedPath), { recursive: true });
872
936
  writeFileSync2(resolvedPath, content, "utf-8");
873
937
  embedInClaudeMd(content, projectRoot);
874
938
  ensureHook(projectRoot);
@@ -908,8 +972,8 @@ async function runRefresh(options) {
908
972
  try {
909
973
  const stat = statSync(outputPath);
910
974
  const fileIsFresh = Date.now() - stat.mtimeMs < 36e5;
911
- const claudeMdPath = join(projectRoot, "CLAUDE.md");
912
- const claudeMd = existsSync4(claudeMdPath) ? readFileSync3(claudeMdPath, "utf-8") : "";
975
+ const claudeMdPath = join2(projectRoot, "CLAUDE.md");
976
+ const claudeMd = existsSync5(claudeMdPath) ? readFileSync4(claudeMdPath, "utf-8") : "";
913
977
  const blockStart = claudeMd.indexOf(CLAUDE_START);
914
978
  const blockEnd = claudeMd.indexOf(CLAUDE_END);
915
979
  const blockContent = blockStart !== -1 && blockEnd !== -1 ? claudeMd.slice(blockStart, blockEnd) : "";
@@ -919,14 +983,14 @@ async function runRefresh(options) {
919
983
  }
920
984
  }
921
985
  const creds = tryResolveAgent(options.envFile);
922
- if (!creds || !existsSync4(resolve2(creds.keyPath))) {
986
+ if (!creds || !existsSync5(resolve2(creds.keyPath))) {
923
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");
924
988
  process.exit(1);
925
989
  }
926
990
  const { agentId, ownerId, keyPath, apiUrl } = creds;
927
991
  let privateKeyPem;
928
992
  try {
929
- privateKeyPem = readFileSync3(keyPath, "utf-8");
993
+ privateKeyPem = readFileSync4(keyPath, "utf-8");
930
994
  } catch {
931
995
  throw new Error(`Cannot read private key: ${keyPath}`);
932
996
  }
@@ -944,17 +1008,17 @@ async function runRefresh(options) {
944
1008
  const json = await res.json();
945
1009
  const platforms = json.data?.platforms ?? [];
946
1010
  const content = generatePermissionsFile(agentId, ownerId, platforms);
947
- mkdirSync2(dirname(outputPath), { recursive: true });
1011
+ mkdirSync2(dirname2(outputPath), { recursive: true });
948
1012
  writeFileSync2(outputPath, content, "utf-8");
949
1013
  embedInClaudeMd(content, projectRoot);
950
1014
  ensureHook(projectRoot);
951
1015
  const detected = detectFrameworks();
952
1016
  const refreshed = [];
953
1017
  if (detected.codex || detected.codexDesktop) {
954
- if (embedPermissionsBlock(join(projectRoot, "AGENTS.md"), content)) refreshed.push("AGENTS.md");
1018
+ if (embedPermissionsBlock(join2(projectRoot, "AGENTS.md"), content)) refreshed.push("AGENTS.md");
955
1019
  }
956
1020
  if (detected.openclaw) {
957
- if (embedPermissionsBlock(join(projectRoot, "SKILL.md"), content)) refreshed.push("SKILL.md");
1021
+ if (embedPermissionsBlock(join2(projectRoot, "SKILL.md"), content)) refreshed.push("SKILL.md");
958
1022
  }
959
1023
  if (!options.quiet) {
960
1024
  const extras = refreshed.length > 0 ? ` (also: ${refreshed.join(", ")})` : "";
@@ -962,9 +1026,9 @@ async function runRefresh(options) {
962
1026
  }
963
1027
  }
964
1028
  function embedPermissionsBlock(filePath, permissionsContent) {
965
- if (!existsSync4(filePath)) return false;
1029
+ if (!existsSync5(filePath)) return false;
966
1030
  try {
967
- const existing = readFileSync3(filePath, "utf-8");
1031
+ const existing = readFileSync4(filePath, "utf-8");
968
1032
  const block = `${PERMISSIONS_START}
969
1033
  ${permissionsContent.trimEnd()}
970
1034
  ${PERMISSIONS_END}`;
@@ -987,18 +1051,18 @@ ${block}
987
1051
  }
988
1052
  }
989
1053
  function embedInClaudeMd(permissionsContent, projectRoot) {
990
- const claudeMdPath = join(projectRoot, "CLAUDE.md");
1054
+ const claudeMdPath = join2(projectRoot, "CLAUDE.md");
991
1055
  const block = `${CLAUDE_START}
992
1056
  ${permissionsContent.trimEnd()}
993
1057
  ${CLAUDE_END}`;
994
- if (!existsSync4(claudeMdPath)) {
1058
+ if (!existsSync5(claudeMdPath)) {
995
1059
  writeFileSync2(claudeMdPath, `# Agent Instructions
996
1060
 
997
1061
  ${block}
998
1062
  `, "utf-8");
999
1063
  return;
1000
1064
  }
1001
- const existing = readFileSync3(claudeMdPath, "utf-8");
1065
+ const existing = readFileSync4(claudeMdPath, "utf-8");
1002
1066
  if (existing.includes(CLAUDE_START)) {
1003
1067
  const replaced = existing.replace(
1004
1068
  new RegExp(`${CLAUDE_START}[\\s\\S]*?${CLAUDE_END}`, "m"),
@@ -1014,11 +1078,11 @@ ${block}
1014
1078
  }
1015
1079
  }
1016
1080
  function ensureHook(projectRoot) {
1017
- const settingsPath = join(projectRoot, ".claude", "settings.local.json");
1081
+ const settingsPath = join2(projectRoot, ".claude", "settings.local.json");
1018
1082
  let settings = {};
1019
- if (existsSync4(settingsPath)) {
1083
+ if (existsSync5(settingsPath)) {
1020
1084
  try {
1021
- settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
1085
+ settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
1022
1086
  } catch {
1023
1087
  return;
1024
1088
  }
@@ -1046,16 +1110,16 @@ function ensureHook(projectRoot) {
1046
1110
  perms.allow = [...allow, ...missing];
1047
1111
  settings.permissions = perms;
1048
1112
  }
1049
- mkdirSync2(dirname(settingsPath), { recursive: true });
1113
+ mkdirSync2(dirname2(settingsPath), { recursive: true });
1050
1114
  writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1051
1115
  ensureMcpJson(projectRoot);
1052
1116
  }
1053
1117
  function ensureMcpJson(projectRoot) {
1054
- const mcpJsonPath = join(projectRoot, ".mcp.json");
1118
+ const mcpJsonPath = join2(projectRoot, ".mcp.json");
1055
1119
  let mcpConfig = {};
1056
- if (existsSync4(mcpJsonPath)) {
1120
+ if (existsSync5(mcpJsonPath)) {
1057
1121
  try {
1058
- mcpConfig = JSON.parse(readFileSync3(mcpJsonPath, "utf-8"));
1122
+ mcpConfig = JSON.parse(readFileSync4(mcpJsonPath, "utf-8"));
1059
1123
  } catch {
1060
1124
  return;
1061
1125
  }
@@ -1254,7 +1318,7 @@ async function writeLocalConfig(config) {
1254
1318
  // src/output/env.ts
1255
1319
  import chalk6 from "chalk";
1256
1320
  import { appendFile as appendFile2, writeFile as writeFile3 } from "fs/promises";
1257
- import { existsSync as existsSync5 } from "fs";
1321
+ import { existsSync as existsSync6 } from "fs";
1258
1322
  import path4 from "path";
1259
1323
  function formatEnvBlock(vars) {
1260
1324
  return Object.entries(vars).map(([key, value]) => {
@@ -1265,7 +1329,7 @@ function formatEnvBlock(vars) {
1265
1329
 
1266
1330
  // src/config/ide.ts
1267
1331
  import { mkdir as mkdir3, writeFile as writeFile4, readFile as readFile2, appendFile as appendFile3, copyFile } from "fs/promises";
1268
- import { existsSync as existsSync6 } from "fs";
1332
+ import { existsSync as existsSync7 } from "fs";
1269
1333
  import path5 from "path";
1270
1334
  import os2 from "os";
1271
1335
  function buildMcpEntry(params) {
@@ -1302,7 +1366,7 @@ function claudeDesktopFriendly() {
1302
1366
  }
1303
1367
  async function mergeJsonFile(filePath, key, value) {
1304
1368
  let existing = {};
1305
- if (existsSync6(filePath)) {
1369
+ if (existsSync7(filePath)) {
1306
1370
  try {
1307
1371
  const text = await readFile2(filePath, "utf8");
1308
1372
  existing = JSON.parse(text);
@@ -1324,8 +1388,8 @@ env = { AGENT_ID = "${params.agentId}", OWNER_ID = "${params.ownerId}", PROXY_UR
1324
1388
  }
1325
1389
  async function appendCodexConfig(configPath) {
1326
1390
  const dir = path5.dirname(configPath);
1327
- if (!existsSync6(dir)) await mkdir3(dir, { recursive: true });
1328
- 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") : "";
1329
1393
  if (existing.includes("[mcp_servers.agentvalet]")) return false;
1330
1394
  return true;
1331
1395
  }
@@ -1334,7 +1398,7 @@ async function writeGlobalFrameworkConfigs(params) {
1334
1398
  const codexDir = path5.join(os2.homedir(), ".codex");
1335
1399
  const codexConfigPath = path5.join(codexDir, "config.toml");
1336
1400
  try {
1337
- if (!existsSync6(codexDir)) await mkdir3(codexDir, { recursive: true });
1401
+ if (!existsSync7(codexDir)) await mkdir3(codexDir, { recursive: true });
1338
1402
  if (await appendCodexConfig(codexConfigPath)) {
1339
1403
  await appendFile3(codexConfigPath, buildCodexTomlBlock(params), "utf8");
1340
1404
  written.push("~/.codex/config.toml");
@@ -1359,7 +1423,7 @@ async function writeCodexProjectConfig(params) {
1359
1423
  return written;
1360
1424
  }
1361
1425
  async function stripAgentvaletFromMcpJson(filePath) {
1362
- if (!existsSync6(filePath)) return false;
1426
+ if (!existsSync7(filePath)) return false;
1363
1427
  try {
1364
1428
  const text = await readFile2(filePath, "utf8");
1365
1429
  const config = JSON.parse(text);
@@ -1383,7 +1447,7 @@ async function cleanProjectScopedEntries() {
1383
1447
  cleaned.push(".cursor/mcp.json (project entry removed)");
1384
1448
  }
1385
1449
  const factoryProjectMcp = path5.join(cwd, ".factory/mcp.json");
1386
- if (existsSync6(factoryProjectMcp)) {
1450
+ if (existsSync7(factoryProjectMcp)) {
1387
1451
  try {
1388
1452
  const text = await readFile2(factoryProjectMcp, "utf8");
1389
1453
  const config = JSON.parse(text);
@@ -1474,7 +1538,7 @@ async function writeIdeConfigs(params) {
1474
1538
  }
1475
1539
  async function mergeFactoryDroidMcpJson(filePath, key, value) {
1476
1540
  let existing = {};
1477
- if (existsSync6(filePath)) {
1541
+ if (existsSync7(filePath)) {
1478
1542
  try {
1479
1543
  const text = await readFile2(filePath, "utf8");
1480
1544
  existing = JSON.parse(text);
@@ -1488,7 +1552,7 @@ async function mergeFactoryDroidMcpJson(filePath, key, value) {
1488
1552
  }
1489
1553
  async function updateGitignoreForFactoryDroid() {
1490
1554
  const gitignorePath = path5.join(process.cwd(), ".gitignore");
1491
- if (!existsSync6(gitignorePath)) return;
1555
+ if (!existsSync7(gitignorePath)) return;
1492
1556
  const existing = await readFile2(gitignorePath, "utf8");
1493
1557
  if (existing.includes(".factory/mcp.json")) return;
1494
1558
  const additions = [
@@ -1870,8 +1934,8 @@ import chalk10 from "chalk";
1870
1934
  // src/commands/uninstall.ts
1871
1935
  import { Command as Command2 } from "commander";
1872
1936
  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";
1937
+ import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync3, rmSync } from "fs";
1938
+ import { resolve as resolve3, join as join3 } from "path";
1875
1939
  import { homedir } from "os";
1876
1940
  var CLAUDE_START2 = "<!-- agentvalet:start -->";
1877
1941
  var CLAUDE_END2 = "<!-- agentvalet:end -->";
@@ -1879,9 +1943,9 @@ var PERMISSIONS_START2 = "<!-- agentvalet:permissions:start -->";
1879
1943
  var PERMISSIONS_END2 = "<!-- agentvalet:permissions:end -->";
1880
1944
  var HOOK_MARKER2 = "agentvalet/register refresh";
1881
1945
  function stripPermissionsBlock(filePath) {
1882
- if (!existsSync7(filePath)) return false;
1946
+ if (!existsSync8(filePath)) return false;
1883
1947
  try {
1884
- const content = readFileSync4(filePath, "utf-8");
1948
+ const content = readFileSync5(filePath, "utf-8");
1885
1949
  if (!content.includes(PERMISSIONS_START2)) return false;
1886
1950
  const stripped = content.replace(new RegExp(`${PERMISSIONS_START2}[\\s\\S]*?${PERMISSIONS_END2}`, "m"), "").replace(/(\r?\n){3,}/g, "\n\n").trimEnd();
1887
1951
  writeFileSync3(filePath, stripped + "\n", "utf-8");
@@ -1892,8 +1956,8 @@ function stripPermissionsBlock(filePath) {
1892
1956
  }
1893
1957
  function stripClaudeMd() {
1894
1958
  const path8 = resolve3("CLAUDE.md");
1895
- if (!existsSync7(path8)) return;
1896
- let content = readFileSync4(path8, "utf-8");
1959
+ if (!existsSync8(path8)) return;
1960
+ let content = readFileSync5(path8, "utf-8");
1897
1961
  content = content.replace(new RegExp(`${CLAUDE_START2}[\\s\\S]*?${CLAUDE_END2}`, "m"), "");
1898
1962
  content = content.replace(/^@\.claude\/agentvalet-permissions\.md\s*$/m, "");
1899
1963
  content = content.replace(/(\r?\n){3,}/g, "\n\n").trim();
@@ -1901,9 +1965,9 @@ function stripClaudeMd() {
1901
1965
  }
1902
1966
  function stripSettingsJson() {
1903
1967
  const path8 = resolve3(".claude/settings.local.json");
1904
- if (!existsSync7(path8)) return;
1968
+ if (!existsSync8(path8)) return;
1905
1969
  try {
1906
- const settings = JSON.parse(readFileSync4(path8, "utf-8"));
1970
+ const settings = JSON.parse(readFileSync5(path8, "utf-8"));
1907
1971
  const hooks = settings.hooks ?? {};
1908
1972
  const ups = hooks.UserPromptSubmit ?? [];
1909
1973
  hooks.UserPromptSubmit = ups.filter(
@@ -1919,9 +1983,9 @@ function stripSettingsJson() {
1919
1983
  }
1920
1984
  }
1921
1985
  function stripMcpJson(filePath, options = {}) {
1922
- if (!existsSync7(filePath)) return false;
1986
+ if (!existsSync8(filePath)) return false;
1923
1987
  try {
1924
- const config = JSON.parse(readFileSync4(filePath, "utf-8"));
1988
+ const config = JSON.parse(readFileSync5(filePath, "utf-8"));
1925
1989
  let removed = false;
1926
1990
  if (options.wrapped === false) {
1927
1991
  if (config["agentvalet"] !== void 0) {
@@ -1945,9 +2009,9 @@ function stripMcpJson(filePath, options = {}) {
1945
2009
  }
1946
2010
  }
1947
2011
  function stripCodexTomlAt(filePath) {
1948
- if (!existsSync7(filePath)) return false;
2012
+ if (!existsSync8(filePath)) return false;
1949
2013
  try {
1950
- const content = readFileSync4(filePath, "utf-8");
2014
+ const content = readFileSync5(filePath, "utf-8");
1951
2015
  if (!content.includes("[mcp_servers.agentvalet]")) return false;
1952
2016
  const stripped = content.replace(
1953
2017
  /\r?\n?\[mcp_servers\.agentvalet\][\s\S]*?(?=\r?\n\[|\r?\n*$)/,
@@ -1960,9 +2024,9 @@ function stripCodexTomlAt(filePath) {
1960
2024
  }
1961
2025
  }
1962
2026
  function isAgentValetGeneratedDoc(filePath, markers) {
1963
- if (!existsSync7(filePath)) return false;
2027
+ if (!existsSync8(filePath)) return false;
1964
2028
  try {
1965
- const content = readFileSync4(filePath, "utf-8");
2029
+ const content = readFileSync5(filePath, "utf-8");
1966
2030
  const firstLine = content.split(/\r?\n/).find((l) => l.trim().length > 0) ?? "";
1967
2031
  return markers.some((m) => firstLine.startsWith(m));
1968
2032
  } catch {
@@ -1974,13 +2038,13 @@ function cleanLocally() {
1974
2038
  const stripped = [];
1975
2039
  const r = (p) => {
1976
2040
  const abs = resolve3(p);
1977
- if (existsSync7(abs)) {
2041
+ if (existsSync8(abs)) {
1978
2042
  rmSync(abs, { recursive: true, force: true });
1979
2043
  removed.push(abs);
1980
2044
  }
1981
2045
  };
1982
2046
  const rAbs = (abs) => {
1983
- if (existsSync7(abs)) {
2047
+ if (existsSync8(abs)) {
1984
2048
  rmSync(abs, { recursive: true, force: true });
1985
2049
  removed.push(abs);
1986
2050
  }
@@ -2003,12 +2067,12 @@ function cleanLocally() {
2003
2067
  if (stripMcpJson(resolve3(".cursor/mcp.json"))) stripped.push(".cursor/mcp.json");
2004
2068
  if (stripMcpJson(resolve3(".factory/mcp.json"), { wrapped: false })) stripped.push(".factory/mcp.json");
2005
2069
  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");
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");
2008
2072
  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");
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");
2012
2076
  if (stripCodexTomlAt(resolve3(".codex/config.toml"))) stripped.push(".codex/config.toml");
2013
2077
  stripClaudeMd();
2014
2078
  stripSettingsJson();
@@ -2113,7 +2177,7 @@ function uninstallCommand() {
2113
2177
  }
2114
2178
  let privateKeyPem;
2115
2179
  try {
2116
- privateKeyPem = readFileSync4(creds.keyPath, "utf-8").trim();
2180
+ privateKeyPem = readFileSync5(creds.keyPath, "utf-8").trim();
2117
2181
  } catch {
2118
2182
  }
2119
2183
  if (!privateKeyPem) {
@@ -2230,13 +2294,13 @@ function revokeCommand() {
2230
2294
  import { Command as Command4 } from "commander";
2231
2295
  import chalk11 from "chalk";
2232
2296
  import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile8 } from "fs/promises";
2233
- import { dirname as dirname2 } from "path";
2297
+ import { dirname as dirname3 } from "path";
2234
2298
 
2235
2299
  // src/lib/claude-config.ts
2236
2300
  import { homedir as homedir2 } from "os";
2237
- import { join as join3 } from "path";
2301
+ import { join as join4 } from "path";
2238
2302
  import { readFile as readFile3, writeFile as writeFile6, mkdir as mkdir4, access } from "fs/promises";
2239
- var CLAUDE_CONFIG_PATH = join3(homedir2(), ".claude", "config");
2303
+ var CLAUDE_CONFIG_PATH = join4(homedir2(), ".claude", "config");
2240
2304
  async function writeKeyToClaudeConfig(agentId, privateKeyPem) {
2241
2305
  let config = {};
2242
2306
  try {
@@ -2247,7 +2311,7 @@ async function writeKeyToClaudeConfig(agentId, privateKeyPem) {
2247
2311
  }
2248
2312
  config.agent_keys = config.agent_keys ?? {};
2249
2313
  config.agent_keys[agentId] = Buffer.from(privateKeyPem).toString("base64");
2250
- await mkdir4(join3(homedir2(), ".claude"), { recursive: true });
2314
+ await mkdir4(join4(homedir2(), ".claude"), { recursive: true });
2251
2315
  await writeFile6(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
2252
2316
  }
2253
2317
 
@@ -2282,7 +2346,7 @@ function injectCommand() {
2282
2346
  "--target <target>",
2283
2347
  "Where to write the key: agentvalet (default \u2014 modern store + MCP configs), env-local (.env.local), claude-legacy (~/.claude/config, deprecated)",
2284
2348
  "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) => {
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) => {
2286
2350
  let privateKeyPem;
2287
2351
  try {
2288
2352
  privateKeyPem = await readFile5(opts.keyFile, "utf-8");
@@ -2302,7 +2366,8 @@ function injectCommand() {
2302
2366
  console.error(chalk11.dim(" Find it at https://app.agentvalet.ai or in your existing ~/.agentvalet/config.json"));
2303
2367
  process.exit(1);
2304
2368
  }
2305
- 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 });
2306
2371
  await writeFile8(AGENT_KEY_PATH, privateKeyPem, { mode: 384 });
2307
2372
  saveConfig({
2308
2373
  agentId: opts.agentId,
@@ -2357,7 +2422,7 @@ function injectCommand() {
2357
2422
  process.exit(1);
2358
2423
  }
2359
2424
  } catch (err) {
2360
- console.error(chalk11.red(String(err)));
2425
+ console.error(chalk11.red(err instanceof KeyClobberError ? err.message : String(err)));
2361
2426
  process.exit(1);
2362
2427
  }
2363
2428
  console.log(chalk11.green("\n Done. Your agent is ready to use."));
@@ -2400,7 +2465,7 @@ async function bindInstallToken(proxyUrl, bootstrapToken) {
2400
2465
  return { ...body, privateKeyPem: privateKey };
2401
2466
  }
2402
2467
  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) => {
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) => {
2404
2469
  const scope = opts.project ? "project" : "user";
2405
2470
  if (!token.startsWith("itok_")) {
2406
2471
  console.error(chalk12.red('Invalid install token. Tokens start with "itok_".'));
@@ -2426,8 +2491,14 @@ function installCommand() {
2426
2491
  const agent_name = claim.agent_id;
2427
2492
  try {
2428
2493
  await mkdir6(AGENT_DIR, { recursive: true });
2494
+ assertSafeToWriteKey(AGENT_KEY_PATH, agent_id, { force: opts.force });
2429
2495
  await writeFile9(AGENT_KEY_PATH, private_key_pem, { mode: 384 });
2430
2496
  } catch (err) {
2497
+ if (err instanceof KeyClobberError) {
2498
+ console.error(chalk12.red(`
2499
+ \u2717 ${err.message}`));
2500
+ process.exit(3);
2501
+ }
2431
2502
  console.error(chalk12.red(`
2432
2503
  \u2717 Failed to write key: ${err instanceof Error ? err.message : err}`));
2433
2504
  process.exit(3);
@@ -2563,14 +2634,14 @@ function installCommand() {
2563
2634
  // src/commands/claim.ts
2564
2635
  import { Command as Command6 } from "commander";
2565
2636
  import { generateKeyPairSync as generateKeyPairSync2 } from "crypto";
2566
- 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";
2567
2638
  import { homedir as homedir3 } from "os";
2568
- import { join as join4 } from "path";
2639
+ import { join as join5 } from "path";
2569
2640
  import chalk13 from "chalk";
2570
2641
  var DEFAULT_PROXY_URL = "https://api.agentvalet.ai";
2571
2642
  function resolveAgentDir(label) {
2572
- const root = join4(homedir3(), ".agentvalet");
2573
- return label ? join4(root, label) : root;
2643
+ const root = join5(homedir3(), ".agentvalet");
2644
+ return label ? join5(root, label) : root;
2574
2645
  }
2575
2646
  function sanitiseLabel(label) {
2576
2647
  return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -2614,9 +2685,9 @@ function claimCommand() {
2614
2685
  process.exit(3);
2615
2686
  }
2616
2687
  const agentDir = resolveAgentDir(label);
2617
- const keyPath = join4(agentDir, "agent.key");
2618
- const identityPath = join4(agentDir, "agent.json");
2619
- if (existsSync8(keyPath)) {
2688
+ const keyPath = join5(agentDir, "agent.key");
2689
+ const identityPath = join5(agentDir, "agent.json");
2690
+ if (existsSync9(keyPath)) {
2620
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.`;
2621
2692
  console.error(isJson ? JSON.stringify({ error: msg }) : chalk13.red(msg));
2622
2693
  process.exit(3);
@@ -2708,7 +2779,7 @@ function collectScope(value, previous) {
2708
2779
  return [...previous, value];
2709
2780
  }
2710
2781
  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) => {
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) => {
2712
2783
  const isJson = !!opts.json;
2713
2784
  let ownerId = opts.owner ?? process.env.AGENTVALET_OWNER ?? "";
2714
2785
  let agentName = opts.name ?? "";
@@ -2824,10 +2895,15 @@ Approve at: https://app.agentvalet.ai/agents
2824
2895
  agentId,
2825
2896
  ownerId: resolvedOwnerId,
2826
2897
  privateKey,
2827
- apiUrl: opts.apiUrl
2898
+ apiUrl: opts.apiUrl,
2899
+ force: !!opts.force
2828
2900
  }));
2829
2901
  } catch (err) {
2830
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
+ }
2831
2907
  console.error(isJson ? JSON.stringify({ error: msg }) : chalk14.red(`Failed to save output: ${msg}`));
2832
2908
  process.exit(3);
2833
2909
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvalet/register",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Self-register AI agents with AgentValet",
5
5
  "type": "module",
6
6
  "bin": {