@agenticmail/api 0.9.14 → 0.9.16

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.
package/dist/index.js CHANGED
@@ -557,6 +557,24 @@ function createAccountRoutes(accountManager, db, config) {
557
557
  next(err);
558
558
  }
559
559
  });
560
+ router.patch("/accounts/:id/host", requireMaster, async (req, res, next) => {
561
+ try {
562
+ const host2 = req.body?.host;
563
+ if (host2 !== null && (typeof host2 !== "string" || !host2.trim())) {
564
+ res.status(400).json({ error: "host must be a non-empty string, or null to unclaim" });
565
+ return;
566
+ }
567
+ const patch = host2 === null ? { host: null } : { host: host2.trim() };
568
+ const updated = await accountManager.updateMetadata(req.params.id, patch);
569
+ if (!updated) {
570
+ res.status(404).json({ error: "Agent not found" });
571
+ return;
572
+ }
573
+ res.json(sanitizeAgent(updated));
574
+ } catch (err) {
575
+ next(err);
576
+ }
577
+ });
560
578
  router.delete("/accounts/:id", requireMaster, async (req, res, next) => {
561
579
  try {
562
580
  const allAgents = await accountManager.list();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/api",
3
- "version": "0.9.14",
3
+ "version": "0.9.16",
4
4
  "description": "REST API server for AgenticMail — email and SMS endpoints for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" fill="#D97757" fill-rule="nonzero"></path></svg>
@@ -0,0 +1 @@
1
+ <svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z"></path></svg>
@@ -1,23 +1,84 @@
1
1
  // Agent identity + avatar helpers.
2
2
  //
3
- // The bridge agent (default name "claudecode") is the host's identity
4
- // inside AgenticMail. We render it with the OFFICIAL Claude starburst
5
- // mark (sourced from the public Wikipedia SVG, served as a static
6
- // asset under /branding/claude-mark.svg) and a green verified-tick so
7
- // the host inbox is recognisable at a glance vs. teammate sub-agents.
3
+ // Each host integration (Claude Code, Codex, …) gets its own branded
4
+ // avatar in the web UI so multiple co-installed bridges can be told
5
+ // apart at a glance. The HOST_BRANDING table below registers one entry
6
+ // per known host integration. Adding a new host = add one row + drop
7
+ // the SVG into /branding/.
8
8
  import { escapeHtml } from './utils.js';
9
9
  import { icon } from './icons.js';
10
10
 
11
- // Official Claude mark, served as a static asset under /branding/.
12
- // Using <img src=...> rather than inlining the path keeps the SVG
13
- // out of every avatar render and lets the browser cache the asset.
14
- const CLAUDE_MARK_URL = '/branding/claude-mark.svg';
11
+ /**
12
+ * Registry of host integrations branding.
13
+ *
14
+ * key metadata.host value (lowercased)
15
+ * logoUrl path under /branding/, served as a static asset
16
+ * altText alt= text on the <img>
17
+ * aliases legacy / alternate names that should map to this host
18
+ * (some bridges historically used the host's product
19
+ * name instead of the host id)
20
+ *
21
+ * The lookup picks an entry by, in order:
22
+ * 1. agent.metadata.host
23
+ * 2. agent.name (so a bridge with name 'codex' but no metadata.host
24
+ * still renders correctly during the upgrade window)
25
+ * 3. any alias listed under a host
26
+ */
27
+ const HOST_BRANDING = {
28
+ claudecode: {
29
+ logoUrl: '/branding/claude-color.svg',
30
+ altText: 'Claude',
31
+ aliases: ['claude'],
32
+ },
33
+ codex: {
34
+ logoUrl: '/branding/openai-mark.svg',
35
+ altText: 'OpenAI Codex',
36
+ aliases: ['openai', 'chatgpt'],
37
+ },
38
+ };
39
+
40
+ // Fallback when we know an account is a bridge but can't identify which
41
+ // host owns it (legacy account with no host tag, unknown host name).
42
+ // Better than mis-attributing — a generic host badge plus a verified
43
+ // tick still reads as "this is a host, not a teammate".
44
+ const GENERIC_HOST_LOGO = '/branding/agenticmail-logo.png';
15
45
 
16
46
  export function isBridgeAgent(agent) {
17
47
  if (!agent) return false;
18
48
  const name = (agent.name ?? '').toLowerCase();
19
49
  const role = (agent.role ?? '').toLowerCase();
20
- return name === 'claudecode' || name === 'claude' || role === 'bridge';
50
+ const meta = agent.metadata ?? {};
51
+ if (role === 'bridge') return true;
52
+ if (meta && meta.bridge === true) return true;
53
+ // Name-based fallback for pre-0.9.3 bridges that still use role='assistant'.
54
+ if (HOST_BRANDING[name]) return true;
55
+ for (const entry of Object.values(HOST_BRANDING)) {
56
+ if (entry.aliases?.includes(name)) return true;
57
+ }
58
+ return false;
59
+ }
60
+
61
+ /**
62
+ * Resolve the host branding entry for an agent. Returns null when the
63
+ * agent isn't a bridge or can't be matched to a known host.
64
+ */
65
+ function brandingFor(agent) {
66
+ if (!agent) return null;
67
+ const name = (agent.name ?? '').toLowerCase();
68
+ const metaHost = (agent.metadata?.host ?? '').toString().toLowerCase();
69
+
70
+ // 1. Trust the host tag first — it's the canonical source of truth.
71
+ if (metaHost && HOST_BRANDING[metaHost]) return HOST_BRANDING[metaHost];
72
+
73
+ // 2. Fall back to matching the bridge name itself.
74
+ if (HOST_BRANDING[name]) return HOST_BRANDING[name];
75
+
76
+ // 3. Check aliases (e.g. a bridge literally named 'claude').
77
+ for (const entry of Object.values(HOST_BRANDING)) {
78
+ if (entry.aliases?.includes(name)) return entry;
79
+ if (entry.aliases?.includes(metaHost)) return entry;
80
+ }
81
+ return null;
21
82
  }
22
83
 
23
84
  // Deterministic colour per agent name — keeps teammate colours stable
@@ -35,7 +96,10 @@ function avatarColorFor(name) {
35
96
  export function avatarHtml(agent, size = '') {
36
97
  const cls = `avatar ${size}`.trim();
37
98
  if (isBridgeAgent(agent)) {
38
- return `<span class="${cls} avatar-host"><img src="${CLAUDE_MARK_URL}" alt="Claude" class="avatar-img" /><span class="avatar-check">${icon('check', { size: 10 })}</span></span>`;
99
+ const brand = brandingFor(agent);
100
+ const url = brand?.logoUrl ?? GENERIC_HOST_LOGO;
101
+ const alt = brand?.altText ?? 'Host';
102
+ return `<span class="${cls} avatar-host"><img src="${url}" alt="${escapeHtml(alt)}" class="avatar-img" /><span class="avatar-check">${icon('check', { size: 10 })}</span></span>`;
39
103
  }
40
104
  const initial = (agent.name ?? '?').slice(0, 1).toUpperCase();
41
105
  const color = avatarColorFor(agent.name ?? '');
@@ -27,6 +27,11 @@ export function renderProfile() {
27
27
  const badge = isBridgeAgent(agent)
28
28
  ? '<span class="role-badge role-badge-host">Host</span>'
29
29
  : '<span class="role-badge role-badge-sub">Sub-agent</span>';
30
+ // Host-ownership badge — shows which LLM the agent rides on.
31
+ // Populated by the MCP server's create_account from
32
+ // AGENTICMAIL_MCP_HOST in the host install's MCP env block.
33
+ // Bridges already show "Host" so we skip the extra chip there.
34
+ const hostTag = !isBridgeAgent(agent) ? hostBadge(agent) : '';
30
35
  const check = selected ? `<span class="selected-check">${icon('check', { size: 20 })}</span>` : '';
31
36
  const unread = state.unread?.[agent.id] ?? 0;
32
37
  const unreadDot = unread > 0
@@ -36,7 +41,7 @@ export function renderProfile() {
36
41
  <div class="profile-menu-item" data-id="${agent.id}">
37
42
  ${avatarHtml(agent, 'avatar-md')}
38
43
  <div class="meta">
39
- <div class="name">${escapeHtml(agent.name)} ${badge} ${unreadDot}</div>
44
+ <div class="name">${escapeHtml(agent.name)} ${badge} ${hostTag} ${unreadDot}</div>
40
45
  <div class="email">${escapeHtml(agent.email ?? '')}</div>
41
46
  </div>
42
47
  ${check}
@@ -45,6 +50,39 @@ export function renderProfile() {
45
50
  }).join('');
46
51
  }
47
52
 
53
+ /**
54
+ * Render a host-ownership badge for an agent. The host name comes from
55
+ * `metadata.host` on the account. Three states:
56
+ *
57
+ * - "Claude" (purple) — owned by the Claude Code dispatcher
58
+ * - "Codex" (orange) — owned by the OpenAI Codex dispatcher
59
+ * - "Unclaimed" (gray) — no host tag yet; legacy or pre-MCP-tagging.
60
+ * Both dispatchers (if both running) will wake on this account.
61
+ * User can claim with `agenticmail-<host> claim <name>`.
62
+ *
63
+ * Returns an empty string when metadata is genuinely absent and we
64
+ * don't want to clutter the row (e.g. the bridge account itself,
65
+ * which already shows "Host").
66
+ */
67
+ function hostBadge(agent) {
68
+ const meta = agent.metadata ?? {};
69
+ const host = typeof meta.host === 'string' ? meta.host.toLowerCase() : '';
70
+ if (host === 'claudecode' || host === 'claude') {
71
+ return '<span class="role-badge role-badge-claude" title="Owned by the Claude Code dispatcher (runs on Anthropic via @anthropic-ai/claude-agent-sdk)">Claude</span>';
72
+ }
73
+ if (host === 'codex') {
74
+ return '<span class="role-badge role-badge-codex" title="Owned by the OpenAI Codex dispatcher (runs on OpenAI via @openai/codex-sdk)">Codex</span>';
75
+ }
76
+ if (host) {
77
+ // Unknown host (forward-compat with Grok / Hermes when they land).
78
+ return `<span class="role-badge role-badge-host-other" title="Owned by the ${escapeHtml(host)} dispatcher">${escapeHtml(host)}</span>`;
79
+ }
80
+ // No host tag — surface the "unclaimed" state explicitly so the user
81
+ // notices and runs `agenticmail-<host> claim` if they have multiple
82
+ // dispatchers running.
83
+ return '<span class="role-badge role-badge-unclaimed" title="No host owner — any dispatcher will wake on this account. Run `agenticmail-<host> claim <name>` to settle ownership.">Unclaimed</span>';
84
+ }
85
+
48
86
  export function toggleProfileMenu(e) {
49
87
  if (e) e.stopPropagation();
50
88
  document.getElementById('profile-menu').classList.toggle('open');
package/public/styles.css CHANGED
@@ -342,8 +342,22 @@ a { color: var(--accent-strong); }
342
342
  }
343
343
  .role-badge-host { background: #fef3c7; color: #92400e; }
344
344
  .role-badge-sub { background: var(--pink-soft); color: var(--accent-strong); }
345
+ /* Host-ownership badges — show which LLM each agent's dispatcher
346
+ rides on. Color-coded by provider so the user can scan an inbox
347
+ list and tell at a glance which agents will wake under which
348
+ model. metadata.host = 'claudecode' → Claude (Anthropic purple);
349
+ 'codex' → Codex (OpenAI orange/green); other → generic; absent →
350
+ "Unclaimed" gray with hint to run `agenticmail-<host> claim`. */
351
+ .role-badge-claude { background: #ede9fe; color: #5b21b6; } /* Anthropic purple */
352
+ .role-badge-codex { background: #dcfce7; color: #166534; } /* OpenAI green */
353
+ .role-badge-host-other { background: #dbeafe; color: #1e40af; } /* unknown future host */
354
+ .role-badge-unclaimed { background: #f3f4f6; color: #6b7280; } /* legacy / no owner */
345
355
  @media (prefers-color-scheme: dark) {
346
- .role-badge-host { background: #44290c; color: #fcd34d; }
356
+ .role-badge-host { background: #44290c; color: #fcd34d; }
357
+ .role-badge-claude { background: #3b1f6b; color: #c4b5fd; }
358
+ .role-badge-codex { background: #14361f; color: #86efac; }
359
+ .role-badge-host-other { background: #1e3a8a; color: #93c5fd; }
360
+ .role-badge-unclaimed { background: #1f2937; color: #9ca3af; }
347
361
  }
348
362
 
349
363
  /* ─── Main grid: sidebar + content ──────────────────────────────── */