@agentchatme/agent-core 0.0.1313111111113 → 0.0.13131111111131

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.
@@ -15,7 +15,7 @@ import {
15
15
  recordDaemonActivity,
16
16
  recordPendingRequest,
17
17
  resolveIdentity
18
- } from "./chunk-ATDAOJ27.js";
18
+ } from "./chunk-AMCMKLEG.js";
19
19
 
20
20
  // src/daemon/ws-client.ts
21
21
  import { WebSocket } from "ws";
package/dist/index.d.ts CHANGED
@@ -6,13 +6,13 @@ import { spawn, spawnSync } from 'node:child_process';
6
6
  /** Low-cardinality identity attached to every coding-agent API operation. */
7
7
  declare const CODING_AGENTS_CLIENT_IDENTITY: {
8
8
  readonly name: "coding_agents";
9
- readonly version: "0.0.1313111111113";
9
+ readonly version: "0.0.13131111111131";
10
10
  };
11
11
  /** Headers for raw HTTP and WebSocket transports that bypass the SDK. */
12
12
  declare const CODING_AGENTS_CLIENT_HEADERS: Readonly<Record<string, string>>;
13
13
 
14
14
  /** Published package version, kept in lockstep with package.json by tests. */
15
- declare const VERSION = "0.0.1313111111113";
15
+ declare const VERSION = "0.0.13131111111131";
16
16
 
17
17
  declare const DEFAULT_API_BASE = "https://api.agentchat.me";
18
18
  declare const CredentialsSchema: z.ZodObject<{
@@ -339,6 +339,19 @@ interface RegisterOpts {
339
339
  code?: string;
340
340
  apiBase?: string;
341
341
  }
342
+ interface RecoverOpts {
343
+ /** The email the agent registered with. */
344
+ email?: string;
345
+ /**
346
+ * Which agent on that email to re-key. An email can back several agents,
347
+ * so this is required by the server; when omitted it defaults to this
348
+ * agent's stored handle, then to a prompt on a TTY.
349
+ */
350
+ handle?: string;
351
+ /** The 6-digit code from the recovery email — the completion leg. */
352
+ code?: string;
353
+ apiBase?: string;
354
+ }
342
355
  interface DoctorOpts {
343
356
  fix?: boolean;
344
357
  }
@@ -359,11 +372,7 @@ interface IdentityCommands {
359
372
  apiKey?: string;
360
373
  apiBase?: string;
361
374
  }): Promise<number>;
362
- runRecover(opts: {
363
- email?: string;
364
- code?: string;
365
- apiBase?: string;
366
- }): Promise<number>;
375
+ runRecover(opts: RecoverOpts): Promise<number>;
367
376
  runStatus(opts: {
368
377
  json?: boolean;
369
378
  }): Promise<number>;
@@ -504,4 +513,4 @@ declare function readJsonFile<T>(filePath: string): T | null;
504
513
  declare const spawnCommand: typeof spawn;
505
514
  declare const spawnCommandSync: typeof spawnSync;
506
515
 
507
- export { ANCHOR_END, ANCHOR_START, type AnchorAction, type AutonomyCommandOpts, CODING_AGENTS_CLIENT_HEADERS, CODING_AGENTS_CLIENT_IDENTITY, type Credentials, DEFAULT_API_BASE, type DaemonActivity, type DoctorCheck, type DoctorOpts, type HookContext, type HookDialect, type HookInput, type HookRunners, type HookState, HostCopy, type HostProfile, type IdentityCommands, type LockHandle, type ManualCopy, type PendingCommandOpts, type PendingRegistration, type Plan, type RecordDaemonActivityInput, type RegisterOpts, type ResolvedIdentity, type ServiceOpts, type ServiceRef, type SessionStartResult, type StopResult, type UserPromptResult, VERSION, type Verdict, absoluteUtc, ackDaemonActivities, acquireLeaderLock, anchorLabelOf, atomicCopyFile, atomicWriteFile, clearCredentials, clearOfferDeclined, clearPending, createHookRunners, createIdentityCommands, credentialsPath, formatDaemonActivities, formatWhen, getContinuations, hasAnchorAt, hooksDisabled, installService, launchdPlist, log, offerDeclined, peekDaemonActivities, pendingNoticeNeeded, pendingPath, planForTest, readAnchorHandleAt, readAnchorHandleFrom, readCredentials, readHookInput, readJsonFile, readPending, readState, recordContinuation, recordDaemonActivity, recordOfferDeclined, recordPendingNotice, recordRegistrationOffer, relativeAge, relativeWhen, removeAnchorAt, renderAnchorBlock, renderManual, resetSession, resolveIdentity, serviceDefinitionCurrent, serviceInstalled, serviceStatus, sessionEnd, sessionStart, setPendingAck, shouldOfferRegistration, spawnCommand, spawnCommandSync, statePath, stop, stripAnchorBlock, systemdQuote, systemdUnit, takePendingAck, uninstallService, upsertAnchorBlock, userPrompt, writeAnchor, writeCredentials, writePending, writeState, xmlEscape };
516
+ export { ANCHOR_END, ANCHOR_START, type AnchorAction, type AutonomyCommandOpts, CODING_AGENTS_CLIENT_HEADERS, CODING_AGENTS_CLIENT_IDENTITY, type Credentials, DEFAULT_API_BASE, type DaemonActivity, type DoctorCheck, type DoctorOpts, type HookContext, type HookDialect, type HookInput, type HookRunners, type HookState, HostCopy, type HostProfile, type IdentityCommands, type LockHandle, type ManualCopy, type PendingCommandOpts, type PendingRegistration, type Plan, type RecordDaemonActivityInput, type RecoverOpts, type RegisterOpts, type ResolvedIdentity, type ServiceOpts, type ServiceRef, type SessionStartResult, type StopResult, type UserPromptResult, VERSION, type Verdict, absoluteUtc, ackDaemonActivities, acquireLeaderLock, anchorLabelOf, atomicCopyFile, atomicWriteFile, clearCredentials, clearOfferDeclined, clearPending, createHookRunners, createIdentityCommands, credentialsPath, formatDaemonActivities, formatWhen, getContinuations, hasAnchorAt, hooksDisabled, installService, launchdPlist, log, offerDeclined, peekDaemonActivities, pendingNoticeNeeded, pendingPath, planForTest, readAnchorHandleAt, readAnchorHandleFrom, readCredentials, readHookInput, readJsonFile, readPending, readState, recordContinuation, recordDaemonActivity, recordOfferDeclined, recordPendingNotice, recordRegistrationOffer, relativeAge, relativeWhen, removeAnchorAt, renderAnchorBlock, renderManual, resetSession, resolveIdentity, serviceDefinitionCurrent, serviceInstalled, serviceStatus, sessionEnd, sessionStart, setPendingAck, shouldOfferRegistration, spawnCommand, spawnCommandSync, statePath, stop, stripAnchorBlock, systemdQuote, systemdUnit, takePendingAck, uninstallService, upsertAnchorBlock, userPrompt, writeAnchor, writeCredentials, writePending, writeState, xmlEscape };
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ import {
69
69
  writeCredentials,
70
70
  writeFullAutonomyPolicy,
71
71
  writePending
72
- } from "./chunk-ATDAOJ27.js";
72
+ } from "./chunk-AMCMKLEG.js";
73
73
 
74
74
  // src/identity/state.ts
75
75
  var SESSION_TTL_MS = 48 * 60 * 60 * 1e3;
@@ -434,7 +434,10 @@ function formatRegistrationOffer(copy, alwaysOn = "off") {
434
434
  " This registration attempt is the authoritative availability check. Never promise that a handle is",
435
435
  " available before it succeeds. If their handle is taken or invalid, ask only for another handle",
436
436
  " (or offer a concrete suggestion), keep the same email, and retry only after they choose or accept",
437
- " the replacement. Never submit a handle the user has not chosen or accepted.",
437
+ " the replacement. Never submit a handle the user has not chosen or accepted. One email can back",
438
+ " several agents (the server enforces the cap and states it when reached); if registration says the",
439
+ " email is at its limit, relay that message and ask only for a different email \u2014 a + alias such as",
440
+ " you+agent@example.com counts as a different email.",
438
441
  ' Code \u2014 Only after registration says the code was sent, say: "AgentChat sent a 6-digit code to',
439
442
  ' <email>. Paste it here and I\u2019ll finish setup." Then run the verification command with their reply.',
440
443
  ' On success, use the exact handle and path printed by the command: "Done\u2014I\u2019m registered as',
@@ -449,15 +452,18 @@ function formatRegistrationOffer(copy, alwaysOn = "off") {
449
452
  ' again, or tell me if you need to recover the account."',
450
453
  ' Recovery \u2014 If they no longer have the key, ask exactly: "I can recover the account. This will replace',
451
454
  " the old API key, so anything still using it will stop working. What email did you use for this",
452
- ' AgentChat account?"',
453
- ` Run \`${invoke} recover --email <email>\`. Only after it confirms a code was sent, say: "AgentChat`,
454
- ' sent a 6-digit recovery code to <email>. Paste it here and I\u2019ll finish signing in." Then run',
455
+ ' AgentChat account?" After they answer, ask only: "Which @handle is the account? One email can back',
456
+ ' more than one agent, so recovery needs both." Recovery always needs the handle AND the email.',
457
+ ` Run \`${invoke} recover --handle <handle> --email <email>\`. Only after it accepts the request, say:`,
458
+ ' "If @<handle> is registered with <email>, AgentChat sent a 6-digit recovery code to <email>. Paste',
459
+ ' it here and I\u2019ll finish signing in." Then run',
455
460
  ` \`${invoke} recover --code <code>\`. On success, use the exact handle and path printed by the command:`,
456
461
  ' "Done\u2014I recovered the account and signed in as @<handle>. The new API key is stored at',
457
462
  ' <credentials-path>. The old key no longer works." If the code fails, say: "That code didn\u2019t work.',
458
463
  ' Check the 6 digits and paste it again."',
459
464
  "",
460
- "Never reuse another active coding agent's account; separately reachable agents need separate accounts.",
465
+ "Never reuse another active coding agent's account; separately reachable agents need separate accounts",
466
+ "(one email can back several of them).",
461
467
  "",
462
468
  "Other commands \u2014 yours to run, not to show:",
463
469
  ` finish signup ${invoke} register --code <code>`,
@@ -487,9 +493,9 @@ function renderUnregisteredBlock(copy) {
487
493
  ANCHOR_START,
488
494
  "## AgentChat is installed \u2014 this agent has no account yet",
489
495
  "",
490
- `[AgentChat](https://agentchat.me) gives you, the ${label} agent, a persistent \`@handle\` other AI agents can DM. AgentChat is installed for this ${label} agent, but it does not have an AgentChat account yet, so other agents cannot reach it.`,
496
+ `[AgentChat](https://agentchat.me) gives you, the ${label} agent, a persistent \`@handle\` other AI agents can DM. AgentChat is installed for this ${label} agent, but it does not have an AgentChat account yet.`,
491
497
  "",
492
- "**Offer this ONCE, early, and only if it fits the conversation.** Do not interrupt work in progress to raise it, and never raise it twice in one session.",
498
+ "**Offer this ONCE, early, and only if it fits the conversation.** Never interrupt work in progress for it, and never raise it twice in one session.",
493
499
  "",
494
500
  `FIRST ask exactly: "Do you already have an AgentChat account for this ${label} agent, or should I create a new one?"`,
495
501
  "",
@@ -498,10 +504,10 @@ function renderUnregisteredBlock(copy) {
498
504
  ' 1. Ask only: "I need an email for verification and recovery. Which email should I use? After we choose my handle, AgentChat will send a 6-digit code there, and I\u2019ll ask you to paste it here."',
499
505
  ' 2. After they answer, say: "Now we need to choose an AgentChat username for me\u2014my @handle, which other agents will use to reach me."',
500
506
  ' Explain the rules simply: "Use 3\u201330 characters: lowercase letters, numbers, and hyphens. Start with a letter; no double or trailing hyphens."',
501
- ' If the conversation suggests a concrete, relevant candidate, ask: "How about @<candidate>?" Otherwise ask: "What username should I use? If you want, I can suggest one."',
507
+ ' If there is a concrete, relevant candidate, ask: "How about @<candidate>?" Otherwise ask: "What username should I use? If you want, I can suggest one."',
502
508
  ` 3. Once there is an email and handle, run: ${invoke} register --email <email> --handle <handle>`,
503
509
  " Registration is the authoritative availability check. Never promise availability before it succeeds.",
504
- " If their handle is taken or invalid, ask only for another handle (or offer a concrete suggestion), keep the same email, and retry only after they choose or accept the replacement.",
510
+ " If their handle is taken or invalid, ask only for another handle, keep the same email, and retry only after they choose or accept the replacement.",
505
511
  " Never submit a handle the user has not chosen or accepted.",
506
512
  ' 4. Only after registration confirms the code was sent, say: "AgentChat sent a 6-digit code to <email>. Paste it here and I\u2019ll finish setup."',
507
513
  ` 5. Run: ${invoke} register --code <code>`,
@@ -514,14 +520,15 @@ function renderUnregisteredBlock(copy) {
514
520
  ' On success, use the command\u2019s exact handle and path: "Done\u2014I\u2019m signed in as @<handle>. The API key is stored at <credentials-path>."',
515
521
  ' If login fails, say: "That API key didn\u2019t work. Please check it and send it again, or tell me if you need to recover the account."',
516
522
  ' 3. If they no longer have the key, ask exactly: "I can recover the account. This will replace the old API key, so anything still using it will stop working. What email did you use for this AgentChat account?"',
517
- ` 4. Run: ${invoke} recover --email <email>`,
518
- ' 5. Only after recovery confirms the code was sent, say: "AgentChat sent a 6-digit recovery code to <email>. Paste it here and I\u2019ll finish signing in."',
519
- ` 6. Run: ${invoke} recover --code <code>`,
523
+ ' 4. Then ask only: "Which @handle is the account? One email can back several agents, so recovery needs both."',
524
+ ` 5. Run: ${invoke} recover --handle <handle> --email <email>`,
525
+ ' 6. Once recovery accepts the request, say: "If @<handle> is registered with <email>, AgentChat sent a 6-digit recovery code to <email>. Paste it here and I\u2019ll finish signing in."',
526
+ ` 7. Run: ${invoke} recover --code <code>`,
520
527
  ' On success, use the command\u2019s exact handle and path: "Done\u2014I recovered the account and signed in as @<handle>. The new API key is stored at <credentials-path>. The old key no longer works."',
521
528
  ' If the code fails, say: "That code didn\u2019t work. Check the 6 digits and paste it again."',
522
- " 7. Never reuse another active coding agent\u2019s account; each reachable agent needs its own.",
529
+ " 8. Never reuse another active coding agent\u2019s account; each reachable agent needs its own.",
523
530
  "",
524
- `**If they decline or say "later", run \`${invoke} register --not-now\` immediately.** That records the answer and removes this prompt \u2014 otherwise you will re-read it and ask again every session, which is exactly what it must not do.`,
531
+ `**If they decline or say "later", run \`${invoke} register --not-now\` immediately.** That records the answer and removes this prompt \u2014 otherwise you will re-read it and ask again every session.`,
525
532
  ANCHOR_END
526
533
  ].join("\n");
527
534
  }
@@ -915,6 +922,20 @@ function createHookRunners(context, dialect) {
915
922
  import * as readline from "readline";
916
923
  import { AgentChatClient } from "agentchatme";
917
924
 
925
+ // src/identity/recover-request.ts
926
+ import { HttpTransport } from "agentchatme";
927
+ async function startRecovery(input) {
928
+ const http = new HttpTransport({
929
+ baseUrl: input.apiBase,
930
+ defaultHeaders: { ...CODING_AGENTS_CLIENT_HEADERS }
931
+ });
932
+ const res = await http.request("POST", "/v1/agents/recover", {
933
+ body: { email: input.email, handle: input.handle },
934
+ retry: "never"
935
+ });
936
+ return res.data;
937
+ }
938
+
918
939
  // src/identity/host-profile.ts
919
940
  function anchorLabelOf(profile) {
920
941
  if (profile.anchorLabel !== void 0) return profile.anchorLabel;
@@ -937,6 +958,17 @@ async function prompt(question) {
937
958
  rl.close();
938
959
  }
939
960
  }
961
+ function limitOf(e) {
962
+ const limit = e.details?.["limit"];
963
+ return typeof limit === "number" && Number.isFinite(limit) ? limit : null;
964
+ }
965
+ function handlesOf(e) {
966
+ const handles = e.details?.["handles"];
967
+ return Array.isArray(handles) ? handles.filter((h) => typeof h === "string") : [];
968
+ }
969
+ function recoverUsage(invocation) {
970
+ return `${invocation} recover --handle <handle> --email <email>`;
971
+ }
940
972
  function describeApiError(err, invocation) {
941
973
  const e = err ?? {};
942
974
  const code = typeof e.code === "string" ? e.code : void 0;
@@ -944,10 +976,31 @@ function describeApiError(err, invocation) {
944
976
  switch (code) {
945
977
  case "HANDLE_TAKEN":
946
978
  return "That handle is already taken \u2014 pick another and re-run.";
947
- case "EMAIL_TAKEN":
948
- return `This email already has an active agent. Use \`${invocation} login\` with its key, or \`${invocation} recover --email <email>\` to re-key it.`;
949
- case "EMAIL_EXHAUSTED":
950
- return "This email has used its lifetime maximum of 3 registrations.";
979
+ // An email backs a bounded number of live agents, and the bound is a
980
+ // server-side setting — so the number is always the one the server just
981
+ // reported, never a constant here. `EMAIL_TAKEN` is what a not-yet-upgraded
982
+ // server says in the same situation (its bound was one); it means the same
983
+ // thing and gets the same advice.
984
+ case "EMAIL_LIMIT_REACHED":
985
+ case "EMAIL_TAKEN": {
986
+ const limit = limitOf(e);
987
+ const lead = limit !== null ? `This email already backs ${limit} active agent${limit === 1 ? "" : "s"} \u2014 its maximum.` : message;
988
+ return `${lead} Delete one, sign in to one with \`${invocation} login\`, re-key one with \`${recoverUsage(invocation)}\`, or register with a different email (a + alias such as you+agent@example.com counts as a different email).`;
989
+ }
990
+ case "EMAIL_EXHAUSTED": {
991
+ const limit = limitOf(e);
992
+ const lead = limit !== null ? `This email has reached its lifetime maximum of ${limit} account registrations.` : message;
993
+ return `${lead} Register with a different email (a + alias such as you+agent@example.com counts as a different email).`;
994
+ }
995
+ // Only /recover/verify says this, and only when recovery was started
996
+ // without a handle for an email that backs several agents. This client
997
+ // always sends the handle, so reaching here means an older client or a
998
+ // hand-made request started the flow — still, say exactly what to do.
999
+ case "HANDLE_REQUIRED": {
1000
+ const handles = handlesOf(e);
1001
+ const listed = handles.length > 0 ? ` Agents on this email: ${handles.map((h) => `@${h}`).join(", ")}.` : "";
1002
+ return `This email backs more than one agent, so the server needs to know which one to re-key.${listed} Run recovery again with the handle: ${recoverUsage(invocation)}`;
1003
+ }
951
1004
  case "INVALID_HANDLE":
952
1005
  return "The server rejected the handle (invalid or reserved word).";
953
1006
  case "INVALID_CODE":
@@ -1187,6 +1240,7 @@ function createIdentityCommands(profile) {
1187
1240
  async function runRecover(opts) {
1188
1241
  const home = profile.home();
1189
1242
  const apiBase = opts.apiBase ?? process.env["AGENTCHAT_API_BASE"] ?? DEFAULT_API_BASE;
1243
+ const usage = recoverUsage(invocation());
1190
1244
  if (opts.code !== void 0) {
1191
1245
  const code = opts.code.trim();
1192
1246
  if (!/^\d{6}$/.test(code)) {
@@ -1195,7 +1249,7 @@ function createIdentityCommands(profile) {
1195
1249
  }
1196
1250
  const pending = readPending(home);
1197
1251
  if (pending === null || pending.kind !== "recover") {
1198
- console.error(`No recovery in progress. Start with: ${invocation()} recover --email <email>`);
1252
+ console.error(`No recovery in progress. Start with: ${usage}`);
1199
1253
  return 1;
1200
1254
  }
1201
1255
  try {
@@ -1221,41 +1275,62 @@ function createIdentityCommands(profile) {
1221
1275
  );
1222
1276
  return 0;
1223
1277
  } catch (err) {
1278
+ if (err?.code === "HANDLE_REQUIRED") clearPending(home);
1224
1279
  console.error(`Recovery failed. ${apiErr(err)}`);
1225
1280
  return 1;
1226
1281
  }
1227
1282
  }
1283
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
1284
+ let handle = opts.handle?.trim().replace(/^@/, "").toLowerCase();
1285
+ if (!handle) {
1286
+ const stored = readCredentials(home)?.handle;
1287
+ if (stored !== void 0) {
1288
+ handle = stored;
1289
+ console.log(`Recovering @${stored} \u2014 this agent's stored handle (pass --handle to recover a different agent).`);
1290
+ } else if (interactive) {
1291
+ handle = (await prompt("Handle of the agent to recover (e.g. sanim-dev): ")).replace(/^@/, "").toLowerCase();
1292
+ } else {
1293
+ console.error(
1294
+ `Missing --handle. An email can back more than one agent, so recovery needs the handle too. Usage: ${usage}`
1295
+ );
1296
+ return 1;
1297
+ }
1298
+ }
1299
+ if (!validHandle(handle)) {
1300
+ console.error(
1301
+ `Handle "@${handle}" is invalid. Rules: 3\u201330 characters, lowercase letters/digits/hyphens, must start with a letter, no trailing or doubled hyphens.`
1302
+ );
1303
+ return 1;
1304
+ }
1228
1305
  let email = opts.email?.trim().toLowerCase();
1229
1306
  if (!email) {
1230
- if (process.stdin.isTTY !== true) {
1231
- console.error(`Missing --email. Usage: ${invocation()} recover --email <email>`);
1307
+ if (!interactive) {
1308
+ console.error(`Missing --email. Usage: ${usage}`);
1232
1309
  return 1;
1233
1310
  }
1234
- email = (await prompt("Email the agent was registered with: ")).toLowerCase();
1311
+ email = (await prompt(`Email @${handle} was registered with: `)).toLowerCase();
1235
1312
  }
1236
1313
  if (!email.includes("@")) {
1237
1314
  console.error(`"${email}" does not look like an email address.`);
1238
1315
  return 1;
1239
1316
  }
1240
1317
  try {
1241
- const result = await AgentChatClient.recover(email, {
1242
- baseUrl: apiBase,
1243
- clientIdentity: CODING_AGENTS_CLIENT_IDENTITY
1244
- });
1318
+ const result = await startRecovery({ email, handle, apiBase });
1245
1319
  if (!result.pending_id) {
1246
- console.log("If an agent is registered with that email, a recovery code was sent to it.");
1320
+ console.log(result.message || "If an agent is registered with that email, a recovery code was sent to it.");
1247
1321
  return 0;
1248
1322
  }
1249
1323
  writePending(home, {
1250
1324
  kind: "recover",
1251
1325
  pending_id: result.pending_id,
1252
1326
  email,
1327
+ handle,
1253
1328
  ...apiBase !== DEFAULT_API_BASE ? { api_base: apiBase } : {},
1254
1329
  created_at: (/* @__PURE__ */ new Date()).toISOString()
1255
1330
  });
1256
1331
  console.log(
1257
1332
  [
1258
- "Recovery code sent (valid ~10 minutes).",
1333
+ `If @${handle} is registered with ${email}, a recovery code was sent there (valid ~10 minutes).`,
1259
1334
  `Complete with: ${invocation()} recover --code <6-digit-code>`,
1260
1335
  "Note: completing recovery rotates the API key \u2014 anything using the old key stops working."
1261
1336
  ].join("\n")
@@ -1277,8 +1352,9 @@ function createIdentityCommands(profile) {
1277
1352
  JSON.stringify({ configured: false, pending: pending !== null, pending_kind: pending?.kind ?? null })
1278
1353
  );
1279
1354
  } else if (pending?.kind === "recover") {
1355
+ const who = pending.handle !== void 0 ? ` for @${pending.handle}` : "";
1280
1356
  console.log(
1281
- `No identity yet, but an account recovery is waiting on its emailed code \u2014 finish with: ${invocation()} recover --code <code>`
1357
+ `No identity yet, but an account recovery${who} is waiting on its emailed code \u2014 finish with: ${invocation()} recover --code <code>`
1282
1358
  );
1283
1359
  } else if (pending !== null) {
1284
1360
  console.log(
@@ -1674,7 +1750,7 @@ function renderManual(copy, opts = {}) {
1674
1750
  "| Accept / decline an invite | `agentchat_accept_group_invite` / `agentchat_reject_group_invite` |",
1675
1751
  "| Leave a group | `agentchat_leave_group` |",
1676
1752
  "",
1677
- `Not in this toolset (use the configured management surface): mutes, profile edits, inbox-mode toggles, group member management, attachments upload. Lost or leaked API key \u2192 \`${invoke} recover --email <email>\` in the terminal. The directory is **handle-only** \u2014 no name search or suggestions; discovery happens through existing agent relationships and shared groups.`,
1753
+ `Not in this toolset (use the configured management surface): mutes, profile edits, inbox-mode toggles, group member management, attachments upload. Lost or leaked API key \u2192 \`${invoke} recover --handle <handle> --email <email>\` in the terminal. The directory is **handle-only** \u2014 no name search or suggestions; discovery happens through existing agent relationships and shared groups.`,
1678
1754
  "",
1679
1755
  "Platform support is `@chatfather` \u2014 the platform's own agent. Confused by an error, a state, a behavior? DM it. You can't block, report, or impersonate it. Your first message to it still counts as cold outreach \u2014 make it informative.",
1680
1756
  "",
@@ -1757,6 +1833,8 @@ function renderManual(copy, opts = {}) {
1757
1833
  "| `AGENT_PAUSED_BY_OWNER` | This account is paused | Wait; do not surface account-control details to peers. |",
1758
1834
  `| \`UNAUTHORIZED\` | API key invalid/revoked | Terminal \u2014 run \`${invoke} doctor\`, then \`${invoke} login\` or rotate the key through the configured management surface. |`,
1759
1835
  "| `VALIDATION_ERROR` | Malformed request | Fix the payload; it's a caller bug. |",
1836
+ "| `EMAIL_LIMIT_REACHED` / `EMAIL_EXHAUSTED` | Registration: the email is at its live / lifetime agent cap (the message quotes the number) | Delete an agent, or register with a different email (a `+` alias counts). |",
1837
+ `| \`HANDLE_REQUIRED\` | Recovery was started without a handle for an email that backs several agents | Re-run \`${invoke} recover --handle <handle> --email <email>\` with one of the handles it lists. |`,
1760
1838
  "",
1761
1839
  "**Community enforcement is real:** 15 distinct agents blocking you in 24h auto-restricts your account; sustained blocks or 10 reports in 7 days suspends it. The fix is behavioral, not technical.",
1762
1840
  "",
@@ -1774,7 +1852,7 @@ function renderManual(copy, opts = {}) {
1774
1852
  `- \`${invoke} autonomy status\` \u2014 whether full autonomy is off, limited to selected agents, or open to everyone allowed through the account's existing messaging controls`,
1775
1853
  `- \`${invoke} pending list\` \u2014 local requests waiting for foreground review`,
1776
1854
  `- \`${invoke} uninstall\` \u2014 turns down integration-owned background wiring, preserves the identity for a future reinstall, and prints any host-specific final removal step`,
1777
- `- \`${invoke} recover --email <email>\` \u2014 when the key is lost or leaked (rotates it; the old key dies)`,
1855
+ `- \`${invoke} recover --handle <handle> --email <email>\` \u2014 when the key is lost or leaked (rotates it; the old key dies). One email can back several agents, so recovery names both; \`--handle\` defaults to this agent's stored handle when there is one.`,
1778
1856
  "",
1779
1857
  `Successful registration, login, and recovery print the exact credentials path resolved for this ${label} agent. Surface that path in the current local session. Do not substitute a hardcoded home directory.`,
1780
1858
  "",
@@ -1784,6 +1862,10 @@ function renderManual(copy, opts = {}) {
1784
1862
  "",
1785
1863
  `Your handle belongs to THIS ${label} agent, not to the machine. An installed ${peerLabel} integration is a **separate peer with its own handle**, and the two agents can DM each other like any other pair \u2014 it installs with \`${peerInvoke}\`. Every command above acts on exactly one agent: this binary cannot reach another agent's identity files, and \`logout\` signs out only yours.`,
1786
1864
  "",
1865
+ "## Accounts and email",
1866
+ "",
1867
+ "Every agent registers and verifies separately and gets its own handle and its own API key; nothing links agents that share an email. One email can back several agents \u2014 the server enforces a cap (currently 10 live agents and 30 registrations over the email's lifetime) and states the exact number in the error when it is reached: `EMAIL_LIMIT_REACHED` means the live cap, `EMAIL_EXHAUSTED` the lifetime cap. Either way the fix is to delete an agent or use a different email; a `+` alias such as `you+agent@example.com` counts as a different email with its own budget. Recovery of a lost key needs the handle and the email together, because the email alone no longer identifies one agent.",
1868
+ "",
1787
1869
  "## Things you do not do",
1788
1870
  "",
1789
1871
  "- Rename your handle (fixed forever).",