amalgm 0.1.143 → 0.1.144

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 (57) hide show
  1. package/package.json +1 -1
  2. package/runtime/lib/local/amalgmStore.js +0 -7
  3. package/runtime/scripts/amalgm-mcp/apps/rest.js +24 -1
  4. package/runtime/scripts/amalgm-mcp/apps/supervisor.js +16 -0
  5. package/runtime/scripts/amalgm-mcp/browser/cli.js +1 -5
  6. package/runtime/scripts/amalgm-mcp/browser/engine.js +18 -3
  7. package/runtime/scripts/amalgm-mcp/browser/paths.js +6 -1
  8. package/runtime/scripts/amalgm-mcp/browser/rest.js +12 -5
  9. package/runtime/scripts/amalgm-mcp/computer-use/index.js +1038 -0
  10. package/runtime/scripts/amalgm-mcp/lib/email-embeds.js +53 -23
  11. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-aa-57534e-ffffff.png +0 -0
  12. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-nb-f37626-ffffff.png +0 -0
  13. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-p-d24726-ffffff.png +0 -0
  14. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-atsign-ffffff.png +0 -0
  15. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-box-d6d3d1.png +0 -0
  16. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-calendarclock-d6d3d1.png +0 -0
  17. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-file-a8a29e.png +0 -0
  18. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-filearchive-facc15.png +0 -0
  19. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-fileimage-2dd4bf.png +0 -0
  20. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-fileimage-c084fc.png +0 -0
  21. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-filespreadsheet-4ade80.png +0 -0
  22. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-filetext-60a5fa.png +0 -0
  23. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-filetext-a8a29e.png +0 -0
  24. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-filetext-f87171.png +0 -0
  25. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-folder-d6d3d1.png +0 -0
  26. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-globe-d6d3d1.png +0 -0
  27. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-video-f472b6.png +0 -0
  28. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-zap-fbbf24.png +0 -0
  29. package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/manifest.json +56 -0
  30. package/runtime/scripts/amalgm-mcp/lib/email-icons.js +34 -1
  31. package/runtime/scripts/amalgm-mcp/lib/email-md.js +95 -51
  32. package/runtime/scripts/amalgm-mcp/lib/file-icon-registry.json +146 -3
  33. package/runtime/scripts/amalgm-mcp/lib/generated/email-render.json +153 -0
  34. package/runtime/scripts/amalgm-mcp/notify/index.js +5 -2
  35. package/runtime/scripts/amalgm-mcp/server/core-tools.js +6 -0
  36. package/runtime/scripts/amalgm-mcp/server/routes/apps.js +4 -0
  37. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-push.test.js +60 -0
  38. package/runtime/scripts/amalgm-mcp/tests/bundle-entries.test.js +54 -0
  39. package/runtime/scripts/amalgm-mcp/tests/computer-use.test.js +151 -0
  40. package/runtime/scripts/amalgm-mcp/tests/core-tools.test.js +4 -0
  41. package/runtime/scripts/amalgm-mcp/tests/email-render-fixtures/citations.md +9 -0
  42. package/runtime/scripts/amalgm-mcp/tests/email-render-fixtures/entities.md +15 -0
  43. package/runtime/scripts/amalgm-mcp/tests/email-render-fixtures/links.md +5 -0
  44. package/runtime/scripts/amalgm-mcp/tests/email-render-fixtures/media.md +21 -0
  45. package/runtime/scripts/amalgm-mcp/tests/email-render-fixtures/text.md +30 -0
  46. package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/citations.html +5 -0
  47. package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/entities.html +8 -0
  48. package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/links.html +3 -0
  49. package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/media.html +11 -0
  50. package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/notification-shell.html +11 -0
  51. package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/text.html +12 -0
  52. package/runtime/scripts/amalgm-mcp/tests/email-render.test.js +82 -0
  53. package/runtime/scripts/amalgm-mcp/tests/mcp-surface.test.js +2 -0
  54. package/runtime/scripts/amalgm-mcp/tests/system-catalog.test.js +5 -0
  55. package/runtime/scripts/amalgm-mcp/toolbox/loadout-context.js +1 -0
  56. package/runtime/scripts/amalgm-mcp/toolbox/mcp-surface.js +1 -0
  57. package/runtime/scripts/amalgm-mcp/toolbox/store.js +18 -0
@@ -20,30 +20,16 @@
20
20
  const embeds = require('./email-embeds');
21
21
  const { buildEmailRenderContext, normalizeEmailLink } = require('./email-deeplinks');
22
22
 
23
- const EMAIL_TOKENS = {
24
- font: {
25
- text: "'SF Pro Text',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',sans-serif",
26
- mono: "'JetBrains Mono','SF Mono',SFMono-Regular,Menlo,Consolas,monospace",
27
- },
28
- type: {
29
- body: 14,
30
- // h1 caps at the section-header size; h4-h6 share the body size.
31
- headings: { 1: 18, 2: 16, 3: 15, 4: 14, 5: 14, 6: 14 },
32
- code: 12,
33
- table: 14,
34
- },
35
- color: {
36
- text: '#ffffff',
37
- textSecondary: '#d6d3d1',
38
- border: '#1c1917', // stone-900 — container borders
39
- tableBorder: '#292524', // stone-800 — table grid (Notion editor standard)
40
- tableHeaderBg: '#141210', // stone-925 — Notion editor header row
41
- bg: '#000000',
42
- bgInset: '#0a0a0a', // inline-code fill
43
- link: '#60a5fa', // blue-400 — hyperlinks + citations everywhere
44
- },
45
- radius: { inline: 6, block: 12 },
46
- };
23
+ // GENERATED from lib/rendering/tokens.ts by
24
+ // scripts/amalgm-mcp/generate-email-render-assets.mts — the deterministic
25
+ // adapter. There is no hand-mirrored token copy anymore; change tokens.ts
26
+ // (or Citation.tsx for the templates below) and regenerate.
27
+ const RENDER_ASSETS = require('./generated/email-render.json');
28
+ const EMAIL_TOKENS = RENDER_ASSETS.tokens;
29
+ const TEMPLATES = RENDER_ASSETS.templates;
30
+ // Markdown block metrics the same values app/generated/markdown-surface.css
31
+ // is generated from, so blocks land at identical positions in both surfaces.
32
+ const BLOCKS = RENDER_ASSETS.blocks;
47
33
 
48
34
  function escapeHtmlAttr(value) {
49
35
  return String(value)
@@ -74,14 +60,57 @@ function parseStandaloneMediaLine(line) {
74
60
  return { alt: match[1], src, title };
75
61
  }
76
62
 
63
+ /**
64
+ * Fill an SSR'd citation template (the real <Citation> component, rendered at
65
+ * asset-generation time with __PLACEHOLDER__ values). label/href/title are
66
+ * escaped here; icon is trusted HTML (a cid: <img> or favicon <img>).
67
+ */
68
+ function fillCitationTemplate(template, { label, href, title, iconHtml }) {
69
+ let html = template;
70
+ html = title
71
+ ? html.replace('__TITLE__', escapeHtmlAttr(title))
72
+ : html.replace(' title="__TITLE__"', '');
73
+ if (href !== undefined) html = html.replace('__HREF__', escapeHtmlAttr(href));
74
+ if (iconHtml !== undefined) html = html.replace('__ICON__', iconHtml);
75
+ return html.replace('__LABEL__', escapeHtmlText(label));
76
+ }
77
+
77
78
  // Inline citation for amalgm:// references (files, folders, skills, agents).
78
- // Codex-style: plain blue text at the surrounding font size — no chip.
79
- // Non-clickable in email (local resources can't open from a mail client);
80
- // the optional description survives as a title tooltip.
81
- function renderAmalgmCitation(label, _kindAndPath, description) {
82
- const titleAttr = description ? ` title="${escapeHtmlAttr(description)}"` : '';
83
- const safeLabel = escapeHtmlText(label);
84
- return `<span style="color:${EMAIL_TOKENS.color.link};white-space:nowrap"${titleAttr}>${safeLabel}</span>`;
79
+ // Same visual as the app: blue inline text led by the file-type icon (CID
80
+ // PNG raster of the same registry glyph the app shows). Non-clickable in
81
+ // email (local resources can't open from a mail client); the optional
82
+ // description survives as a title tooltip.
83
+ function renderAmalgmCitation(label, kindAndPath, description) {
84
+ const kind = String(kindAndPath || '').split('/')[0].toLowerCase();
85
+ let descriptor = null;
86
+ if (kind === 'file') descriptor = embeds.resolveFileIconDescriptor(label);
87
+ else if (kind === 'folder') descriptor = embeds.resolveFileIconDescriptor('', { isDirectory: true });
88
+ else if (kind === 'skill') descriptor = embeds.resolveFileIconDescriptor('', { isSkill: true });
89
+ else if (kind === 'agent') descriptor = embeds.resolveEntityIconDescriptor('agent');
90
+ if (!descriptor) {
91
+ return fillCitationTemplate(TEMPLATES.citationText, { label, title: description });
92
+ }
93
+ return fillCitationTemplate(TEMPLATES.citationTextWithIcon, {
94
+ label,
95
+ title: description,
96
+ iconHtml: embeds.citationIconHtml(descriptor),
97
+ });
98
+ }
99
+
100
+ /** Web hyperlink as the app renders it: favicon + blue label (Citation + WebFavicon). */
101
+ function renderWebCitation(label, href) {
102
+ let domain = '';
103
+ try {
104
+ domain = new URL(href).hostname;
105
+ } catch {
106
+ domain = '';
107
+ }
108
+ if (!domain) {
109
+ return fillCitationTemplate(TEMPLATES.citationLink, { label, href });
110
+ }
111
+ const size = RENDER_ASSETS.chrome.citationIconSize;
112
+ const favicon = `<img src="https://www.google.com/s2/favicons?domain=${escapeHtmlAttr(domain)}&amp;sz=32" alt="" width="${size}" height="${size}" style="display:inline-block;width:${size}px;height:${size}px;border:0;border-radius:3px;vertical-align:-1px">`;
113
+ return fillCitationTemplate(TEMPLATES.citationLinkWithIcon, { label, href, iconHtml: favicon });
85
114
  }
86
115
 
87
116
  /**
@@ -124,12 +153,12 @@ function inlineFormat(text, renderContext = {}) {
124
153
  (_, label, kindAndPath, description) =>
125
154
  renderAmalgmCitation(label, kindAndPath, description),
126
155
  )
127
- // Links: [text](url) — blue hyperlink per the rendering ruleset.
156
+ // Links: [text](url) — the app's web citation: favicon + blue label.
128
157
  .replace(
129
158
  /\[([^\]]+)\]\(([^)]+)\)/g,
130
159
  (_, label, url) => {
131
160
  const href = normalizeEmailLink(url, renderContext.appOrigin) || url;
132
- return `<a href="${escapeHtmlAttr(href)}" style="color:${EMAIL_TOKENS.color.link};text-decoration:none" target="_blank">${escapeHtmlText(label)}</a>`;
161
+ return renderWebCitation(label, href);
133
162
  },
134
163
  )
135
164
  // Bold + italic: ***text***
@@ -145,7 +174,7 @@ function inlineFormat(text, renderContext = {}) {
145
174
  // Inline code: `text`
146
175
  .replace(
147
176
  /`([^`]+)`/g,
148
- `<code style="background:${EMAIL_TOKENS.color.bgInset};border:1px solid ${EMAIL_TOKENS.color.border};padding:2px 6px;border-radius:${EMAIL_TOKENS.radius.inline}px;font-size:${EMAIL_TOKENS.type.code}px;font-family:${EMAIL_TOKENS.font.mono};color:#f5f5f4">$1</code>`,
177
+ `<code style="background:${BLOCKS.codeInline.bg};border:1px solid ${BLOCKS.codeInline.border};padding:${BLOCKS.codeInline.paddingY}px ${BLOCKS.codeInline.paddingX}px;border-radius:${BLOCKS.codeInline.radius}px;font-size:${BLOCKS.codeInline.size}px;line-height:${BLOCKS.codeInline.lineHeight};font-family:${EMAIL_TOKENS.font.mono};color:${BLOCKS.codeInline.color}">$1</code>`,
149
178
  )
150
179
  );
151
180
  }
@@ -157,12 +186,18 @@ function markdownToEmailHtml(md, renderContextInput = {}) {
157
186
  text = text.replace(/\\n/g, '\n');
158
187
 
159
188
  // Extract fenced code blocks first so line parser doesn't touch them.
189
+ // Chrome mirrors the chat CodeBlock: rounded card with a language header
190
+ // bar (the copy/download controls are app-only interactivity).
160
191
  const codeBlocks = [];
161
- text = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_, _lang, code) => {
192
+ text = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
162
193
  const escaped = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
163
194
  const placeholder = `%%CODEBLOCK_${codeBlocks.length}%%`;
195
+ const B = BLOCKS.codeBlock;
164
196
  codeBlocks.push(
165
- `<pre style="background:${EMAIL_TOKENS.color.bg};border:1px solid ${EMAIL_TOKENS.color.border};border-radius:${EMAIL_TOKENS.radius.block}px;padding:16px;font-size:${EMAIL_TOKENS.type.code}px;font-family:${EMAIL_TOKENS.font.mono};color:#f5f5f4;overflow-x:auto;margin:16px 0;line-height:1.6"><code>${escaped.trimEnd()}</code></pre>`,
197
+ `<div style="background:${B.bg};border:1px solid ${B.border};border-radius:${B.radius}px;overflow:hidden;margin:0">` +
198
+ `<div style="padding:${B.header.paddingY}px ${B.header.paddingX}px;border-bottom:1px solid ${B.border};font-size:${B.header.size}px;color:${B.header.color};font-family:${EMAIL_TOKENS.font.mono};text-transform:lowercase">${escapeHtmlText(lang || 'text')}</div>` +
199
+ `<pre style="background:${B.bg};padding:${B.padding}px;font-size:${B.size}px;font-family:${EMAIL_TOKENS.font.mono};color:${EMAIL_TOKENS.color.text};overflow-x:auto;margin:0;line-height:${B.lineHeight}"><code>${escaped.trimEnd()}</code></pre>` +
200
+ `</div>`,
166
201
  );
167
202
  return placeholder;
168
203
  });
@@ -178,18 +213,21 @@ function markdownToEmailHtml(md, renderContextInput = {}) {
178
213
 
179
214
  function flushPara() {
180
215
  if (!paraLines.length) return;
181
- const content = inlineFormat(paraLines.join('\n'), renderContext).replace(/\n/g, '<br>');
182
- out.push(`<p style="margin:12px 0;line-height:1.7;color:#ffffff">${content}</p>`);
216
+ // Soft line breaks collapse to spaces, like the app's markdown pipeline.
217
+ const content = inlineFormat(paraLines.join(' '), renderContext);
218
+ out.push(
219
+ `<p style="margin:0;line-height:${BLOCKS.paragraph.lineHeight};color:${EMAIL_TOKENS.color.text}">${content}</p>`,
220
+ );
183
221
  paraLines = [];
184
222
  }
185
223
  function flushList() {
186
224
  if (!listItems.length) return;
187
225
  const tag = listType;
188
226
  const items = listItems
189
- .map((t) => `<li style="margin:4px 0;color:#ffffff">${inlineFormat(t, renderContext)}</li>`)
227
+ .map((t) => `<li style="margin:${BLOCKS.list.itemMarginY}px 0;padding:${BLOCKS.list.itemPaddingY}px 0;line-height:${BLOCKS.list.itemLineHeight};color:${EMAIL_TOKENS.color.text}">${inlineFormat(t, renderContext)}</li>`)
190
228
  .join('');
191
229
  out.push(
192
- `<${tag} style="margin:12px 0;padding-left:24px;color:#ffffff">${items}</${tag}>`,
230
+ `<${tag} style="margin:0;padding-left:${BLOCKS.list.paddingLeft}px;color:${EMAIL_TOKENS.color.text}">${items}</${tag}>`,
193
231
  );
194
232
  listItems = [];
195
233
  listType = '';
@@ -204,8 +242,8 @@ function markdownToEmailHtml(md, renderContextInput = {}) {
204
242
  // stone-925 header row, px-3 py-1.5 cells, 14px text, wrapped in the
205
243
  // same rounded-xl (12px) chrome as code blocks. The wrapper draws the
206
244
  // outer border, so cells only draw their inner right/bottom edges.
207
- const cellBase = `padding:6px 12px;text-align:left;vertical-align:top;font-size:${EMAIL_TOKENS.type.table}px`;
208
- const edge = `1px solid ${EMAIL_TOKENS.color.tableBorder}`;
245
+ const cellBase = `padding:${BLOCKS.table.cellPaddingY}px ${BLOCKS.table.cellPaddingX}px;text-align:left;vertical-align:top;font-size:${BLOCKS.table.size}px;line-height:${BLOCKS.table.cellLineHeight}`;
246
+ const edge = `1px solid ${BLOCKS.table.gridColor}`;
209
247
  const cellBorders = (isLastCol, isLastRow) =>
210
248
  `${isLastCol ? '' : `border-right:${edge};`}${isLastRow ? '' : `border-bottom:${edge};`}`;
211
249
  const headerCells = tableRows[0].cells;
@@ -213,7 +251,7 @@ function markdownToEmailHtml(md, renderContextInput = {}) {
213
251
  const hdr = headerCells
214
252
  .map(
215
253
  (c, i) =>
216
- `<th style="${cellBase};${cellBorders(i === headerCells.length - 1, false)}background:${EMAIL_TOKENS.color.tableHeaderBg};font-weight:500;color:#ffffff">${inlineFormat(c, renderContext)}</th>`,
254
+ `<th style="${cellBase};${cellBorders(i === headerCells.length - 1, false)}background:${BLOCKS.table.headerBgEmail};font-weight:${BLOCKS.table.headerWeight};color:${EMAIL_TOKENS.color.text}">${inlineFormat(c, renderContext)}</th>`,
217
255
  )
218
256
  .join('');
219
257
  const body = bodyRows
@@ -230,15 +268,16 @@ function markdownToEmailHtml(md, renderContextInput = {}) {
230
268
  )
231
269
  .join('');
232
270
  out.push(
233
- `<div style="border:${edge};border-radius:12px;overflow:hidden;margin:16px 0"><table style="width:100%;border-collapse:collapse"><thead><tr>${hdr}</tr></thead><tbody>${body}</tbody></table></div>`,
271
+ `<div style="border:${edge};border-radius:${BLOCKS.table.radius}px;overflow:hidden;margin:0"><table style="width:100%;border-collapse:collapse"><thead><tr>${hdr}</tr></thead><tbody>${body}</tbody></table></div>`,
234
272
  );
235
273
  tableRows = [];
236
274
  }
237
275
  function flushQuote() {
238
276
  if (!quoteLines.length) return;
239
- const content = inlineFormat(quoteLines.join('\n'), renderContext).replace(/\n/g, '<br>');
277
+ // Soft line breaks collapse to spaces, like the app's markdown pipeline.
278
+ const content = inlineFormat(quoteLines.join(' '), renderContext);
240
279
  out.push(
241
- `<blockquote style="margin:16px 0;padding:8px 16px;border-left:3px solid ${EMAIL_TOKENS.color.tableBorder};color:${EMAIL_TOKENS.color.textSecondary};font-style:italic">${content}</blockquote>`,
280
+ `<blockquote style="margin:0;line-height:${BLOCKS.blockquote.lineHeight};${BLOCKS.blockquote.italic ? 'font-style:italic;' : ''}padding:0 0 0 ${BLOCKS.blockquote.paddingLeft}px;border-left:${BLOCKS.blockquote.borderWidth}px solid ${BLOCKS.blockquote.borderColor};color:${BLOCKS.blockquote.color}">${content}</blockquote>`,
242
281
  );
243
282
  quoteLines = [];
244
283
  }
@@ -272,7 +311,7 @@ function markdownToEmailHtml(md, renderContextInput = {}) {
272
311
  }
273
312
  if (/^(-{3,}|_{3,}|\*{3,})$/.test(trimmed)) {
274
313
  flushAll();
275
- out.push(`<hr style="border:none;border-top:1px solid ${EMAIL_TOKENS.color.border};margin:24px 0">`);
314
+ out.push(`<hr style="border:none;border-top:1px solid ${BLOCKS.hr.color};margin:0">`);
276
315
  continue;
277
316
  }
278
317
  // Standalone media line → block embed (image bare, video in the
@@ -295,9 +334,9 @@ function markdownToEmailHtml(md, renderContextInput = {}) {
295
334
  if (headingMatch) {
296
335
  flushAll();
297
336
  const level = headingMatch[1].length;
298
- const size = EMAIL_TOKENS.type.headings[level];
337
+ const spec = BLOCKS.heading[`h${Math.min(level, 4)}`];
299
338
  out.push(
300
- `<h${level} style="margin:20px 0 8px;font-size:${size}px;font-weight:650;color:#ffffff;line-height:1.35">${inlineFormat(headingMatch[2], renderContext)}</h${level}>`,
339
+ `<h${level} style="margin:0;font-size:${spec.size}px;font-weight:${BLOCKS.heading.weight};color:${EMAIL_TOKENS.color.text};line-height:${BLOCKS.heading.lineHeight}">${inlineFormat(headingMatch[2], renderContext)}</h${level}>`,
301
340
  );
302
341
  continue;
303
342
  }
@@ -349,7 +388,12 @@ function markdownToEmailHtml(md, renderContextInput = {}) {
349
388
  }
350
389
 
351
390
  flushAll();
352
- return out.join('\n');
391
+ // Uniform gap between top-level blocks — the chat surface's Streamdown
392
+ // container applies space-y (15px at the app root font-size) with the
393
+ // first child's top margin removed; individual blocks carry margin:0.
394
+ return out
395
+ .map((html, index) => (index === 0 ? html : `<div style="margin:${BLOCKS.blockGap}px 0 0">${html}</div>`))
396
+ .join('\n');
353
397
  }
354
398
 
355
399
  /**
@@ -363,7 +407,7 @@ function formatNotificationEmail(message, link, renderContextInput = {}) {
363
407
  : '';
364
408
  return `<!DOCTYPE html>
365
409
  <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
366
- <body style="margin:0;padding:0;background:#000000;font-family:${EMAIL_TOKENS.font.text};-webkit-font-smoothing:antialiased">
410
+ <body style="margin:0;padding:0;background:#000000;font-family:${EMAIL_TOKENS.font.text}">
367
411
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#000000"><tr><td align="center" style="padding:0">
368
412
  <table role="presentation" width="520" cellpadding="0" cellspacing="0" style="max-width:520px;width:100%"><tr><td style="padding:40px 28px 48px">
369
413
  <div style="font-size:${EMAIL_TOKENS.type.body}px;color:#ffffff">${htmlMessage}</div>
@@ -14,9 +14,10 @@
14
14
  "color": "#d6d3d1"
15
15
  },
16
16
  "skill": {
17
- "kind": "skill",
18
- "colorClass": "text-violet-400",
19
- "color": "#a78bfa"
17
+ "kind": "lucide",
18
+ "name": "Box",
19
+ "colorClass": "text-stone-300",
20
+ "color": "#d6d3d1"
20
21
  }
21
22
  },
22
23
  "extensionIcons": {
@@ -418,6 +419,126 @@
418
419
  "name": "FileArchive",
419
420
  "colorClass": "text-yellow-400",
420
421
  "color": "#facc15"
422
+ },
423
+ "pptx": {
424
+ "kind": "letter",
425
+ "letters": "P",
426
+ "bg": "#D24726",
427
+ "fg": "#ffffff"
428
+ },
429
+ "ppt": {
430
+ "kind": "letter",
431
+ "letters": "P",
432
+ "bg": "#D24726",
433
+ "fg": "#ffffff"
434
+ },
435
+ "ipynb": {
436
+ "kind": "letter",
437
+ "letters": "Nb",
438
+ "bg": "#F37626",
439
+ "fg": "#ffffff"
440
+ },
441
+ "ttf": {
442
+ "kind": "letter",
443
+ "letters": "Aa",
444
+ "bg": "#57534E",
445
+ "fg": "#ffffff"
446
+ },
447
+ "otf": {
448
+ "kind": "letter",
449
+ "letters": "Aa",
450
+ "bg": "#57534E",
451
+ "fg": "#ffffff"
452
+ },
453
+ "woff": {
454
+ "kind": "letter",
455
+ "letters": "Aa",
456
+ "bg": "#57534E",
457
+ "fg": "#ffffff"
458
+ },
459
+ "woff2": {
460
+ "kind": "letter",
461
+ "letters": "Aa",
462
+ "bg": "#57534E",
463
+ "fg": "#ffffff"
464
+ },
465
+ "m4a": {
466
+ "kind": "lucide",
467
+ "name": "Music",
468
+ "colorClass": "text-pink-400",
469
+ "color": "#f472b6"
470
+ },
471
+ "flac": {
472
+ "kind": "lucide",
473
+ "name": "Music",
474
+ "colorClass": "text-pink-400",
475
+ "color": "#f472b6"
476
+ },
477
+ "ogg": {
478
+ "kind": "lucide",
479
+ "name": "Music",
480
+ "colorClass": "text-pink-400",
481
+ "color": "#f472b6"
482
+ },
483
+ "aac": {
484
+ "kind": "lucide",
485
+ "name": "Music",
486
+ "colorClass": "text-pink-400",
487
+ "color": "#f472b6"
488
+ },
489
+ "opus": {
490
+ "kind": "lucide",
491
+ "name": "Music",
492
+ "colorClass": "text-pink-400",
493
+ "color": "#f472b6"
494
+ },
495
+ "avif": {
496
+ "kind": "lucide",
497
+ "name": "FileImage",
498
+ "colorClass": "text-purple-400",
499
+ "color": "#c084fc"
500
+ },
501
+ "bmp": {
502
+ "kind": "lucide",
503
+ "name": "FileImage",
504
+ "colorClass": "text-purple-400",
505
+ "color": "#c084fc"
506
+ },
507
+ "avi": {
508
+ "kind": "lucide",
509
+ "name": "Video",
510
+ "colorClass": "text-pink-400",
511
+ "color": "#f472b6"
512
+ },
513
+ "ogv": {
514
+ "kind": "lucide",
515
+ "name": "Video",
516
+ "colorClass": "text-pink-400",
517
+ "color": "#f472b6"
518
+ },
519
+ "jsonl": {
520
+ "kind": "lucide",
521
+ "name": "Braces",
522
+ "colorClass": "text-yellow-400",
523
+ "color": "#facc15"
524
+ },
525
+ "ndjson": {
526
+ "kind": "lucide",
527
+ "name": "Braces",
528
+ "colorClass": "text-yellow-400",
529
+ "color": "#facc15"
530
+ },
531
+ "diff": {
532
+ "kind": "lucide",
533
+ "name": "FileText",
534
+ "colorClass": "text-stone-400",
535
+ "color": "#a8a29e"
536
+ },
537
+ "patch": {
538
+ "kind": "lucide",
539
+ "name": "FileText",
540
+ "colorClass": "text-stone-400",
541
+ "color": "#a8a29e"
421
542
  }
422
543
  },
423
544
  "specialFilenameIcons": [
@@ -553,6 +674,12 @@
553
674
  "name": "Box",
554
675
  "colorClass": "text-white",
555
676
  "color": "#ffffff"
677
+ },
678
+ "agent": {
679
+ "kind": "lucide",
680
+ "name": "AtSign",
681
+ "colorClass": "text-white",
682
+ "color": "#ffffff"
556
683
  }
557
684
  },
558
685
  "lucideNodes": {
@@ -990,6 +1117,22 @@
990
1117
  "d": "M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"
991
1118
  }
992
1119
  ]
1120
+ ],
1121
+ "AtSign": [
1122
+ [
1123
+ "circle",
1124
+ {
1125
+ "cx": "12",
1126
+ "cy": "12",
1127
+ "r": "4"
1128
+ }
1129
+ ],
1130
+ [
1131
+ "path",
1132
+ {
1133
+ "d": "M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"
1134
+ }
1135
+ ]
993
1136
  ]
994
1137
  }
995
1138
  }
@@ -0,0 +1,153 @@
1
+ {
2
+ "//": "GENERATED by scripts/amalgm-mcp/generate-email-render-assets.mts — do not edit.",
3
+ "version": 1,
4
+ "tokens": {
5
+ "font": {
6
+ "text": "\"SF Pro Text\", \"SF Pro Display\", -apple-system, BlinkMacSystemFont, system-ui, sans-serif",
7
+ "mono": "'JetBrains Mono', 'SFMono-Regular', Menlo, monospace"
8
+ },
9
+ "type": {
10
+ "body": 14,
11
+ "headings": {
12
+ "1": 18,
13
+ "2": 16,
14
+ "3": 15,
15
+ "4": 14,
16
+ "5": 14,
17
+ "6": 14
18
+ },
19
+ "code": 12,
20
+ "table": 14
21
+ },
22
+ "color": {
23
+ "text": "#ffffff",
24
+ "textSecondary": "#d6d3d1",
25
+ "textMuted": "#a8a29e",
26
+ "faint": "#57534e",
27
+ "border": "#1c1917",
28
+ "tableBorder": "#292524",
29
+ "tableHeaderBg": "#141210",
30
+ "bg": "#000000",
31
+ "bgInset": "#0a0a0a",
32
+ "bgChip": "#0c0a09",
33
+ "link": "#60a5fa",
34
+ "linkHover": "#93c5fd"
35
+ },
36
+ "radius": {
37
+ "inline": 6,
38
+ "block": 12,
39
+ "embed": 16
40
+ }
41
+ },
42
+ "chrome": {
43
+ "pillTableCss": "display:inline-table;height:32px;background:#0c0a09;border:1px solid #292524;border-radius:12px;border-collapse:separate;box-shadow:0 25px 50px -12px rgba(0,0,0,0.25)",
44
+ "pillHeight": 32,
45
+ "pillFontSize": 13,
46
+ "pillIconSize": 14,
47
+ "pillPaddingX": 12,
48
+ "pillGap": 8,
49
+ "pillText": "#ffffff",
50
+ "frameCss": "border:1px solid #292524;border-radius:16px;background:#000000;margin:0;overflow:hidden",
51
+ "frameBg": "#000000",
52
+ "entityTileCss": "text-decoration:none;display:inline-block;background:#000000;border:1px solid #1c1917;border-radius:12px;padding:8px 12px;margin:2px 0;vertical-align:middle",
53
+ "entityTileFontSize": 14,
54
+ "entityTileFontWeight": 650,
55
+ "entityTileIconSize": 14,
56
+ "entityTileText": "#ffffff",
57
+ "citationIconSize": 12
58
+ },
59
+ "blocks": {
60
+ "blockGap": 15,
61
+ "heading": {
62
+ "lineHeight": 1.35,
63
+ "weight": 650,
64
+ "h1": {
65
+ "size": 18,
66
+ "marginTop": 20,
67
+ "marginBottom": 8
68
+ },
69
+ "h2": {
70
+ "size": 16,
71
+ "marginTop": 18,
72
+ "marginBottom": 6
73
+ },
74
+ "h3": {
75
+ "size": 15,
76
+ "marginTop": 16,
77
+ "marginBottom": 6
78
+ },
79
+ "h4": {
80
+ "size": 14,
81
+ "marginTop": 14,
82
+ "marginBottom": 4
83
+ }
84
+ },
85
+ "paragraph": {
86
+ "lineHeight": 1.75,
87
+ "marginY": 4
88
+ },
89
+ "list": {
90
+ "marginY": 8,
91
+ "paddingLeft": 24,
92
+ "itemMarginY": 4,
93
+ "itemLineHeight": 1.5,
94
+ "itemPaddingY": 3.75
95
+ },
96
+ "blockquote": {
97
+ "marginY": 14,
98
+ "borderWidth": 3,
99
+ "borderColor": "#292524",
100
+ "paddingLeft": 16,
101
+ "color": "#d6d3d1",
102
+ "italic": true,
103
+ "lineHeight": 1.75
104
+ },
105
+ "codeInline": {
106
+ "size": 12,
107
+ "lineHeight": 1.2,
108
+ "radius": 5.625,
109
+ "border": "#1c1917",
110
+ "bg": "#0a0a0a",
111
+ "color": "#f5f5f4",
112
+ "paddingX": 7.5,
113
+ "paddingY": 1.875
114
+ },
115
+ "codeBlock": {
116
+ "size": 12,
117
+ "lineHeight": 1.6,
118
+ "marginY": 16,
119
+ "radius": 12,
120
+ "border": "#1c1917",
121
+ "bg": "#000000",
122
+ "padding": 15,
123
+ "header": {
124
+ "paddingX": 15,
125
+ "paddingY": 7.5,
126
+ "size": 11.25,
127
+ "color": "#a8a29e"
128
+ }
129
+ },
130
+ "table": {
131
+ "size": 14,
132
+ "marginY": 16,
133
+ "radius": 12,
134
+ "gridColor": "#292524",
135
+ "headerBg": "rgb(20 18 16 / 0.6)",
136
+ "headerWeight": 500,
137
+ "cellPaddingX": 11.25,
138
+ "cellPaddingY": 5.625,
139
+ "cellLineHeight": 1.5,
140
+ "headerBgEmail": "#0c0b0a"
141
+ },
142
+ "hr": {
143
+ "color": "#1c1917",
144
+ "marginY": 24
145
+ }
146
+ },
147
+ "templates": {
148
+ "citationLink": "<a href=\"__HREF__\" target=\"_blank\" rel=\"noreferrer\" title=\"__TITLE__\" class=\"render-citation\" style=\"display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;\">__LABEL__</a>",
149
+ "citationLinkWithIcon": "<a href=\"__HREF__\" target=\"_blank\" rel=\"noreferrer\" title=\"__TITLE__\" class=\"render-citation\" style=\"display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;\"><span style=\"display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle\">__ICON__</span>__LABEL__</a>",
150
+ "citationText": "<span title=\"__TITLE__\" class=\"render-citation\" style=\"display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;\">__LABEL__</span>",
151
+ "citationTextWithIcon": "<span title=\"__TITLE__\" class=\"render-citation\" style=\"display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;\"><span style=\"display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle\">__ICON__</span>__LABEL__</span>"
152
+ }
153
+ }
@@ -9,7 +9,7 @@
9
9
  const { PROXY_BASE_URL, PROXY_TOKEN } = require('../config');
10
10
  const { ensureFreshProxyToken, readProxyToken } = require('../../proxy-token-store');
11
11
  const { formatNotificationEmail } = require('../lib/email-md');
12
- const { prepareEmailIconAttachments } = require('../lib/email-icons');
12
+ const { prepareEmailIconAttachments, stripUnattachedIconImages } = require('../lib/email-icons');
13
13
  const { prepareEmailMedia } = require('./email-media');
14
14
  const { textResult, errorResult } = require('../lib/tool-result');
15
15
 
@@ -32,11 +32,14 @@ async function notifyUserViaProxy({ subject: customSubject, message, link, sessi
32
32
  // Local media embeds become attachments riding inside the email: images
33
33
  // inline via cid:, videos as regular attachments with a poster card.
34
34
  const media = prepareEmailMedia(message);
35
- const html = formatNotificationEmail(media.message, link || null, { sessionId });
35
+ let html = formatNotificationEmail(media.message, link || null, { sessionId });
36
36
  const attachments = [
37
37
  ...media.attachments,
38
38
  ...prepareEmailIconAttachments(html, media.attachments),
39
39
  ];
40
+ // Icons that didn't fit the attachment budget degrade to plain text
41
+ // instead of broken-image boxes.
42
+ html = stripUnattachedIconImages(html, attachments);
40
43
 
41
44
  try {
42
45
  const res = await fetch(notifyUrl, {
@@ -58,6 +58,12 @@ const CORE_TOOL_GROUPS = [
58
58
  'Register, run, and route local-first Amalgm apps.',
59
59
  require('../apps/tools'),
60
60
  ),
61
+ group(
62
+ 'computer-use',
63
+ 'Computer Use',
64
+ require('../computer-use').DESCRIPTION,
65
+ require('../computer-use'),
66
+ ),
61
67
  // Browser is ONE toolbox entry. Users enable/disable it per agent as a
62
68
  // single tool; fine-grained control stays available because loadouts can
63
69
  // hold individual action ids (`browser.cua_click`). Action list, capability
@@ -45,6 +45,10 @@ async function handleAppRoutes(ctx) {
45
45
  await appsRest.handleDelete(await ctx.readJsonBody(), ctx.sendJson, options);
46
46
  return true;
47
47
  }
48
+ if (route.path === '/update' && ctx.method === 'POST') {
49
+ await appsRest.handleUpdate(await ctx.readJsonBody(), ctx.sendJson, options);
50
+ return true;
51
+ }
48
52
  if (route.path === '/connect-dns' && ctx.method === 'POST') {
49
53
  await appsRest.handleConnectDns(await ctx.readJsonBody(), ctx.sendJson, options);
50
54
  return true;