amalgm 0.1.246 → 0.1.247
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/README.md +5 -3
- package/lib/cli.js +92 -4
- package/lib/shared-realtime-tunnel.js +82 -19
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/automations/deliveries.js +276 -0
- package/runtime/scripts/amalgm-mcp/automations/rest.js +51 -0
- package/runtime/scripts/amalgm-mcp/automations/scheduler.js +11 -67
- package/runtime/scripts/amalgm-mcp/automations/store.js +67 -20
- package/runtime/scripts/amalgm-mcp/events/ingress.js +69 -215
- package/runtime/scripts/amalgm-mcp/events/processor.js +189 -0
- package/runtime/scripts/amalgm-mcp/lib/email-embeds.js +41 -0
- package/runtime/scripts/amalgm-mcp/lib/email-md.js +107 -1
- package/runtime/scripts/amalgm-mcp/lib/generated/email-render.json +10 -2
- package/runtime/scripts/amalgm-mcp/observer/README.md +8 -6
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +262 -22
- package/runtime/scripts/amalgm-mcp/registration/entity-content.js +86 -0
- package/runtime/scripts/amalgm-mcp/registration/index.js +4 -1
- package/runtime/scripts/amalgm-mcp/registration/service.js +102 -0
- package/runtime/scripts/amalgm-mcp/registration/tree.js +19 -5
- package/runtime/scripts/amalgm-mcp/registry/index.js +16 -1
- package/runtime/scripts/amalgm-mcp/registry/store.js +58 -7
- package/runtime/scripts/amalgm-mcp/server/routes/automations.js +8 -0
- package/runtime/scripts/amalgm-mcp/server/routes/entities.js +56 -0
- package/runtime/scripts/amalgm-mcp/server/routes/workspace.js +14 -0
- package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +1 -1
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +2 -2
- package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/citations.html +5 -5
- package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/links.html +3 -3
- package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/notification-shell.html +1 -1
- package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/share-invite.html +13 -0
- package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/text.html +1 -1
- package/runtime/scripts/amalgm-mcp/tests/email-render.test.js +17 -1
- package/runtime/scripts/amalgm-mcp/tests/entity-cloud.test.js +189 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-content.test.js +126 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-materialization.test.js +118 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +126 -11
- package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +20 -6
- package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +8 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +8 -1
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +630 -102
- package/runtime/scripts/amalgm-mcp/tests/registration.test.js +45 -10
- package/runtime/scripts/amalgm-mcp/tests/workspace-materialize.test.js +54 -0
- package/runtime/scripts/amalgm-mcp/workspace/materialize.js +292 -0
- package/runtime/scripts/chat-core/contract.js +21 -3
- package/runtime/scripts/chat-core/engine.js +8 -0
- package/runtime/scripts/chat-core/server.js +11 -1
- package/runtime/scripts/chat-core/tests/workspace-preflight.test.js +116 -0
- package/runtime/scripts/chat-core/workspace-preflight.js +89 -0
- package/runtime/scripts/local-gateway.js +0 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure delivery-to-run transition for a durable event inbox row.
|
|
5
|
+
*
|
|
6
|
+
* The HTTP route never calls this directly before persistence. A thrown error
|
|
7
|
+
* means the delivery is retryable; returned terminalStatus values mean the
|
|
8
|
+
* immutable input has reached a terminal disposition.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
extractSignature,
|
|
13
|
+
matchAllBySignature,
|
|
14
|
+
matchBySignature,
|
|
15
|
+
matchesEventRef,
|
|
16
|
+
pickSourceLabel,
|
|
17
|
+
collectPassthroughHeaders,
|
|
18
|
+
} = require('./matcher');
|
|
19
|
+
const {
|
|
20
|
+
listEventTriggersForIngress,
|
|
21
|
+
markTriggerFired,
|
|
22
|
+
recordAutomationRun,
|
|
23
|
+
} = require('../automations/store');
|
|
24
|
+
const { executeAutomationForTrigger } = require('../automations/runner');
|
|
25
|
+
const ring = require('./ring-buffer');
|
|
26
|
+
|
|
27
|
+
function cleanRefPart(value) {
|
|
28
|
+
if (typeof value !== 'string') return '';
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
if (!trimmed || trimmed.length > 128) return '';
|
|
31
|
+
return trimmed;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function dayStamp(date = new Date()) {
|
|
35
|
+
return date.toISOString().slice(0, 10).replace(/-/g, '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function recordUnmatchedEventRuns(authenticated, eventSource, eventName, timestamp) {
|
|
39
|
+
const refSlug = `${eventSource}.${eventName}`.toLowerCase().replace(/[^a-z0-9._-]+/g, '_').slice(0, 80);
|
|
40
|
+
const seenAutomations = new Set();
|
|
41
|
+
for (const trigger of authenticated) {
|
|
42
|
+
if (!trigger.automationId || seenAutomations.has(trigger.automationId)) continue;
|
|
43
|
+
seenAutomations.add(trigger.automationId);
|
|
44
|
+
try {
|
|
45
|
+
recordAutomationRun(trigger.automationId, {
|
|
46
|
+
runId: `unmatched-${trigger.automationId}-${refSlug}-${dayStamp()}`,
|
|
47
|
+
automationId: trigger.automationId,
|
|
48
|
+
triggerId: trigger.id,
|
|
49
|
+
triggerName: trigger.name,
|
|
50
|
+
status: 'unmatched',
|
|
51
|
+
startedAt: timestamp,
|
|
52
|
+
finishedAt: timestamp,
|
|
53
|
+
output: { received: { source: eventSource, event: eventName, receivedAt: timestamp } },
|
|
54
|
+
error: `Received ${eventSource}.${eventName} — authenticated, but no trigger selector matched (trigger "${trigger.name}" wants ${trigger.source || '*'}.${trigger.event || '*'}).`,
|
|
55
|
+
}, { source: 'automation_runs:event-unmatched' });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.warn('[AmalgmMCP:Events] Failed to record unmatched event run:', error.message || error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function eventReference(headers, parsedBody) {
|
|
63
|
+
const githubEvent = headers['x-github-event'];
|
|
64
|
+
const vendorSource = pickSourceLabel(headers);
|
|
65
|
+
const vendorEvent = githubEvent
|
|
66
|
+
|| headers['x-linear-event']
|
|
67
|
+
|| headers['x-gitlab-event']
|
|
68
|
+
|| null;
|
|
69
|
+
let source = vendorSource;
|
|
70
|
+
let event = vendorEvent || 'webhook';
|
|
71
|
+
let fallback = null;
|
|
72
|
+
|
|
73
|
+
if (vendorSource === 'external' && !vendorEvent) {
|
|
74
|
+
const headerSource = cleanRefPart(headers['x-amalgm-source']);
|
|
75
|
+
const headerEvent = cleanRefPart(headers['x-amalgm-event']);
|
|
76
|
+
const bodySource = cleanRefPart(typeof parsedBody === 'object' ? parsedBody?.source : null);
|
|
77
|
+
const bodyEvent = cleanRefPart(typeof parsedBody === 'object' ? parsedBody?.event : null);
|
|
78
|
+
if (headerSource || headerEvent) {
|
|
79
|
+
source = headerSource || 'external';
|
|
80
|
+
event = headerEvent || 'webhook';
|
|
81
|
+
} else if (bodySource || bodyEvent) {
|
|
82
|
+
source = bodySource || 'external';
|
|
83
|
+
event = bodyEvent || 'webhook';
|
|
84
|
+
fallback = { source: 'external', event: 'webhook' };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { source, event, fallback, githubPing: githubEvent === 'ping' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function processEventEnvelope(input = {}) {
|
|
92
|
+
const rawBody = typeof input.rawBody === 'string' ? input.rawBody : '';
|
|
93
|
+
const headers = input.headers && typeof input.headers === 'object' ? input.headers : {};
|
|
94
|
+
const signature = extractSignature(headers);
|
|
95
|
+
if (!signature) {
|
|
96
|
+
return {
|
|
97
|
+
terminalStatus: 'rejected',
|
|
98
|
+
disposition: 'unsigned',
|
|
99
|
+
reason: 'Webhook signature required',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const triggers = listEventTriggersForIngress();
|
|
104
|
+
let parsedBody;
|
|
105
|
+
try {
|
|
106
|
+
parsedBody = JSON.parse(rawBody);
|
|
107
|
+
} catch {
|
|
108
|
+
parsedBody = rawBody;
|
|
109
|
+
}
|
|
110
|
+
const ref = eventReference(headers, parsedBody);
|
|
111
|
+
|
|
112
|
+
if (ref.githubPing) {
|
|
113
|
+
const pingTrigger = matchBySignature(triggers, signature, rawBody);
|
|
114
|
+
return pingTrigger
|
|
115
|
+
? { terminalStatus: 'completed', disposition: 'ping', triggerIds: [] }
|
|
116
|
+
: { terminalStatus: 'rejected', disposition: 'invalid-signature', reason: 'Invalid webhook signature' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const authenticated = matchAllBySignature(triggers, signature, rawBody);
|
|
120
|
+
if (!authenticated?.length) {
|
|
121
|
+
return {
|
|
122
|
+
terminalStatus: 'rejected',
|
|
123
|
+
disposition: 'invalid-signature',
|
|
124
|
+
reason: 'Invalid webhook signature',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let source = ref.source;
|
|
129
|
+
let event = ref.event;
|
|
130
|
+
let matched = authenticated.filter((trigger) => matchesEventRef(trigger, { source, event }));
|
|
131
|
+
if (!matched.length && ref.fallback) {
|
|
132
|
+
const fallbackMatches = authenticated.filter((trigger) => matchesEventRef(trigger, ref.fallback));
|
|
133
|
+
if (fallbackMatches.length > 0) {
|
|
134
|
+
source = ref.fallback.source;
|
|
135
|
+
event = ref.fallback.event;
|
|
136
|
+
matched = fallbackMatches;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const timestamp = input.receivedAt || new Date().toISOString();
|
|
141
|
+
const eventObj = {
|
|
142
|
+
id: input.deliveryId || null,
|
|
143
|
+
source,
|
|
144
|
+
event,
|
|
145
|
+
payload: parsedBody,
|
|
146
|
+
headers: collectPassthroughHeaders(headers),
|
|
147
|
+
timestamp,
|
|
148
|
+
receivedAt: timestamp,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
if (!matched.length) {
|
|
152
|
+
ring.push({ ...eventObj, disposition: 'unmatched' });
|
|
153
|
+
recordUnmatchedEventRuns(authenticated, source, event, timestamp);
|
|
154
|
+
return {
|
|
155
|
+
terminalStatus: 'completed',
|
|
156
|
+
disposition: 'unmatched',
|
|
157
|
+
event: eventObj,
|
|
158
|
+
triggerIds: [],
|
|
159
|
+
authenticatedTriggerIds: authenticated.map((trigger) => trigger.id),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
ring.push(eventObj);
|
|
164
|
+
const completed = new Set(Array.isArray(input.completedTriggerIds) ? input.completedTriggerIds : []);
|
|
165
|
+
for (const trigger of matched) {
|
|
166
|
+
if (completed.has(trigger.id)) continue;
|
|
167
|
+
const runs = await executeAutomationForTrigger(trigger.id, eventObj);
|
|
168
|
+
const failed = runs.find((run) => run && ['failed', 'rejected', 'interrupted'].includes(run.status));
|
|
169
|
+
if (failed) {
|
|
170
|
+
throw new Error(failed.error || `Automation run ${failed.id || trigger.id} failed`);
|
|
171
|
+
}
|
|
172
|
+
markTriggerFired(trigger.id, { source: 'events:delivery-completed', firedAt: timestamp });
|
|
173
|
+
input.onTriggerComplete?.(trigger.id);
|
|
174
|
+
completed.add(trigger.id);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
terminalStatus: 'completed',
|
|
179
|
+
disposition: 'triggered',
|
|
180
|
+
event: eventObj,
|
|
181
|
+
triggerIds: matched.map((trigger) => trigger.id),
|
|
182
|
+
completedTriggerIds: [...completed],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
eventReference,
|
|
188
|
+
processEventEnvelope,
|
|
189
|
+
};
|
|
@@ -482,6 +482,46 @@ function renderEntityTile(label, type, id, renderContext = {}) {
|
|
|
482
482
|
);
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
+
/**
|
|
486
|
+
* Full-width share-invite tile — the approved email CTA form of ChatRow.
|
|
487
|
+
* The whole row is one link, with the canonical entity/file icon on the left
|
|
488
|
+
* and the canonical ExternalLink icon on the right. Tables keep the three
|
|
489
|
+
* cells aligned in Gmail and Outlook.
|
|
490
|
+
*/
|
|
491
|
+
function renderShareEntityTile(label, type, href) {
|
|
492
|
+
const safeLabel = escapeText(label || entityTypeLabel(type));
|
|
493
|
+
const normalizedType = type === 'symlink' ? 'file' : type;
|
|
494
|
+
const iconDescriptor = normalizedType === 'file'
|
|
495
|
+
? resolveFileIconDescriptor(label)
|
|
496
|
+
: normalizedType === 'folder'
|
|
497
|
+
? resolveFileIconDescriptor('', { isDirectory: true })
|
|
498
|
+
: resolveEntityIconDescriptor(normalizedType);
|
|
499
|
+
const openDescriptor = {
|
|
500
|
+
kind: 'lucide',
|
|
501
|
+
name: 'ExternalLink',
|
|
502
|
+
colorClass: 'text-white',
|
|
503
|
+
color: COLOR.text,
|
|
504
|
+
};
|
|
505
|
+
const size = CHROME.entityTileIconSize;
|
|
506
|
+
const iconContentIdValue = iconContentId(iconDescriptor);
|
|
507
|
+
const openContentId = iconContentId(openDescriptor);
|
|
508
|
+
const iconStyle = `display:inline-block;width:${size}px;height:${size}px;border:0`;
|
|
509
|
+
return (
|
|
510
|
+
`<a href="${escapeAttr(href)}" target="_blank" ` +
|
|
511
|
+
`style="text-decoration:none;display:block;background:${CHROME.entityTileBg};` +
|
|
512
|
+
`border:1px solid ${CHROME.entityTileBorder};border-radius:${CHROME.entityTileRadius}px;` +
|
|
513
|
+
`padding:0;margin:${CHROME.entityTileMarginY}px 0">` +
|
|
514
|
+
`<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse"><tr>` +
|
|
515
|
+
`<td style="padding:${CHROME.entityTilePaddingY}px 0 ${CHROME.entityTilePaddingY}px ${CHROME.entityTilePaddingX}px;vertical-align:middle;line-height:0;width:${size}px">` +
|
|
516
|
+
`<img src="cid:${iconContentIdValue}" alt="" width="${size}" height="${size}" style="${iconStyle}"></td>` +
|
|
517
|
+
`<td style="padding:${CHROME.entityTilePaddingY}px ${CHROME.entityTilePaddingX}px ${CHROME.entityTilePaddingY}px ${CHROME.entityTileGap}px;vertical-align:middle">` +
|
|
518
|
+
`<span style="font-size:${CHROME.entityTileFontSize}px;line-height:${CHROME.entityTileLineHeight}px;color:${CHROME.entityTileText};font-weight:${CHROME.entityTileFontWeight};white-space:nowrap">${safeLabel}</span></td>` +
|
|
519
|
+
`<td align="right" style="padding:${CHROME.entityTilePaddingY}px ${CHROME.entityTilePaddingX}px ${CHROME.entityTilePaddingY}px 0;vertical-align:middle;line-height:0;width:${size}px">` +
|
|
520
|
+
`<img src="cid:${openContentId}" alt="" width="${size}" height="${size}" style="${iconStyle}"></td>` +
|
|
521
|
+
`</tr></table></a>`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
485
525
|
/**
|
|
486
526
|
* Expanded entity — the email form of the full: render. Email can't iframe
|
|
487
527
|
* the live app, so it's the EmbedChrome card with the entity identity and a
|
|
@@ -527,4 +567,5 @@ module.exports = {
|
|
|
527
567
|
renderFileCardEmbed,
|
|
528
568
|
renderMediaEmbed,
|
|
529
569
|
renderEntityEmbed,
|
|
570
|
+
renderShareEntityTile,
|
|
530
571
|
};
|
|
@@ -26,6 +26,7 @@ const { buildEmailRenderContext, normalizeEmailLink } = require('./email-deeplin
|
|
|
26
26
|
// (or Citation.tsx for the templates below) and regenerate.
|
|
27
27
|
const RENDER_ASSETS = require('./generated/email-render.json');
|
|
28
28
|
const EMAIL_TOKENS = RENDER_ASSETS.tokens;
|
|
29
|
+
const CHROME = RENDER_ASSETS.chrome;
|
|
29
30
|
const TEMPLATES = RENDER_ASSETS.templates;
|
|
30
31
|
// Markdown block metrics — the same values app/generated/markdown-surface.css
|
|
31
32
|
// is generated from, so blocks land at identical positions in both surfaces.
|
|
@@ -418,4 +419,109 @@ function formatNotificationEmail(message, link, renderContextInput = {}) {
|
|
|
418
419
|
</body></html>`;
|
|
419
420
|
}
|
|
420
421
|
|
|
421
|
-
|
|
422
|
+
const SHARE_ENTITY_COPY = {
|
|
423
|
+
file: { phrase: 'a file', noun: 'file' },
|
|
424
|
+
symlink: { phrase: 'a file', noun: 'file' },
|
|
425
|
+
folder: { phrase: 'a folder', noun: 'folder' },
|
|
426
|
+
agent: { phrase: 'an agent', noun: 'agent' },
|
|
427
|
+
automation: { phrase: 'an automation', noun: 'automation' },
|
|
428
|
+
event: { phrase: 'an automation', noun: 'automation' },
|
|
429
|
+
trigger: { phrase: 'an automation', noun: 'automation' },
|
|
430
|
+
chat: { phrase: 'a chat', noun: 'chat' },
|
|
431
|
+
conversation: { phrase: 'a chat', noun: 'chat' },
|
|
432
|
+
app: { phrase: 'an app', noun: 'app' },
|
|
433
|
+
artifact: { phrase: 'an app', noun: 'app' },
|
|
434
|
+
service: { phrase: 'an app', noun: 'app' },
|
|
435
|
+
task: { phrase: 'a task', noun: 'task' },
|
|
436
|
+
preview: { phrase: 'a preview', noun: 'preview' },
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
function shareEntityCopy(entityType) {
|
|
440
|
+
return SHARE_ENTITY_COPY[entityType] || { phrase: 'something', noun: 'item' };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function shareInviteContextHtml({ entityType, access, context }) {
|
|
444
|
+
const paragraph = (content) =>
|
|
445
|
+
`<p style="margin:0;line-height:${BLOCKS.paragraph.lineHeight};color:${EMAIL_TOKENS.color.text}">${content}</p>`;
|
|
446
|
+
if (context && typeof context === 'object') {
|
|
447
|
+
const before = escapeHtmlText(context.before || '');
|
|
448
|
+
const emphasis = escapeHtmlText(context.emphasis || '');
|
|
449
|
+
const after = escapeHtmlText(context.after || '');
|
|
450
|
+
return paragraph(
|
|
451
|
+
before +
|
|
452
|
+
(emphasis
|
|
453
|
+
? `<strong style="color:${EMAIL_TOKENS.color.text};font-weight:600">${emphasis}</strong>`
|
|
454
|
+
: '') +
|
|
455
|
+
after,
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
if (typeof context === 'string' && context.trim()) {
|
|
459
|
+
return paragraph(escapeHtmlText(context.trim()));
|
|
460
|
+
}
|
|
461
|
+
if (access === 'edit' || access === 'view') {
|
|
462
|
+
const strong = `<strong style="color:${EMAIL_TOKENS.color.text};font-weight:600">${access}</strong>`;
|
|
463
|
+
return entityType === 'folder'
|
|
464
|
+
? paragraph(`You can ${strong} everything inside.`)
|
|
465
|
+
: paragraph(`You can ${strong} this ${shareEntityCopy(entityType).noun}.`);
|
|
466
|
+
}
|
|
467
|
+
if (entityType === 'agent') {
|
|
468
|
+
return paragraph('Its tools and skills come with it — everything installs when you accept.');
|
|
469
|
+
}
|
|
470
|
+
return '';
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Approved share/invite email shell. It uses the same generated tokens,
|
|
475
|
+
* Markdown block metrics, entity tile, and CID icon pipeline as notify_user,
|
|
476
|
+
* while keeping the tile as the only CTA.
|
|
477
|
+
*/
|
|
478
|
+
function formatShareInviteEmail({
|
|
479
|
+
sharerName,
|
|
480
|
+
recipientEmail,
|
|
481
|
+
entityName,
|
|
482
|
+
entityType,
|
|
483
|
+
inviteUrl,
|
|
484
|
+
access,
|
|
485
|
+
context,
|
|
486
|
+
}) {
|
|
487
|
+
const normalizedType = String(entityType || 'entity').toLowerCase();
|
|
488
|
+
const copy = shareEntityCopy(normalizedType);
|
|
489
|
+
const headline =
|
|
490
|
+
`<h2 style="margin:0;font-size:${EMAIL_TOKENS.type.headings['2']}px;font-weight:400;` +
|
|
491
|
+
`color:${EMAIL_TOKENS.color.text};line-height:${BLOCKS.heading.lineHeight}">` +
|
|
492
|
+
`<strong style="font-weight:${CHROME.entityTileFontWeight}">${escapeHtmlText(sharerName)}</strong> ` +
|
|
493
|
+
`shared ${copy.phrase} with you</h2>`;
|
|
494
|
+
const tile = embeds.renderShareEntityTile(entityName, normalizedType, inviteUrl);
|
|
495
|
+
const contextHtml = shareInviteContextHtml({ entityType: normalizedType, access, context });
|
|
496
|
+
const finePrint =
|
|
497
|
+
`<blockquote style="margin:0;line-height:${BLOCKS.blockquote.lineHeight};` +
|
|
498
|
+
`${BLOCKS.blockquote.italic ? 'font-style:italic;' : ''}` +
|
|
499
|
+
`padding:0 0 0 ${BLOCKS.blockquote.paddingLeft}px;` +
|
|
500
|
+
`border-left:${BLOCKS.blockquote.borderWidth}px solid ${BLOCKS.blockquote.borderColor};` +
|
|
501
|
+
`color:${BLOCKS.blockquote.color}">${escapeHtmlText(recipientEmail)} — ` +
|
|
502
|
+
`sign in with this address to accept.</blockquote>`;
|
|
503
|
+
const bodyBlocks = [headline, tile, contextHtml, finePrint].filter(Boolean);
|
|
504
|
+
const bodyHtml = bodyBlocks
|
|
505
|
+
.map((block, index) =>
|
|
506
|
+
index === 0 ? block : `<div style="margin:${BLOCKS.blockGap}px 0 0">${block}</div>`,
|
|
507
|
+
)
|
|
508
|
+
.join('\n');
|
|
509
|
+
const textFont = EMAIL_TOKENS.font.text.replace(/"/g, "'");
|
|
510
|
+
|
|
511
|
+
return `<!DOCTYPE html>
|
|
512
|
+
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
|
513
|
+
<body style="margin:0;padding:0;background:${EMAIL_TOKENS.color.bg};font-family:${textFont}">
|
|
514
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:${EMAIL_TOKENS.color.bg}"><tr><td align="center" style="padding:0">
|
|
515
|
+
<table role="presentation" width="520" cellpadding="0" cellspacing="0" style="max-width:520px;width:100%;background:${EMAIL_TOKENS.color.bg};border:1px solid ${EMAIL_TOKENS.color.tableBorder};border-radius:8px;border-collapse:separate;overflow:hidden"><tr><td style="padding:40px 28px 48px">
|
|
516
|
+
<div style="font-size:${EMAIL_TOKENS.type.body}px;color:${EMAIL_TOKENS.color.text}">${bodyHtml}</div>
|
|
517
|
+
<p style="margin:48px 0 0;font-size:12px;color:${EMAIL_TOKENS.color.text}">amalgm.ai</p>
|
|
518
|
+
</td></tr></table>
|
|
519
|
+
</td></tr></table>
|
|
520
|
+
</body></html>`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
module.exports = {
|
|
524
|
+
markdownToEmailHtml,
|
|
525
|
+
formatNotificationEmail,
|
|
526
|
+
formatShareInviteEmail,
|
|
527
|
+
};
|
|
@@ -51,8 +51,16 @@
|
|
|
51
51
|
"frameBg": "#000000",
|
|
52
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
53
|
"entityTileFontSize": 14,
|
|
54
|
+
"entityTileLineHeight": 18,
|
|
54
55
|
"entityTileFontWeight": 650,
|
|
55
56
|
"entityTileIconSize": 14,
|
|
57
|
+
"entityTilePaddingX": 12,
|
|
58
|
+
"entityTilePaddingY": 8,
|
|
59
|
+
"entityTileGap": 10,
|
|
60
|
+
"entityTileMarginY": 2,
|
|
61
|
+
"entityTileRadius": 12,
|
|
62
|
+
"entityTileBorder": "#1c1917",
|
|
63
|
+
"entityTileBg": "#000000",
|
|
56
64
|
"entityTileText": "#ffffff",
|
|
57
65
|
"citationIconSize": 12
|
|
58
66
|
},
|
|
@@ -146,8 +154,8 @@
|
|
|
146
154
|
},
|
|
147
155
|
"templates": {
|
|
148
156
|
"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
|
|
157
|
+
"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:-0.1em\">__ICON__</span>__LABEL__</a>",
|
|
150
158
|
"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
|
|
159
|
+
"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:-0.1em\">__ICON__</span>__LABEL__</span>"
|
|
152
160
|
}
|
|
153
161
|
}
|
|
@@ -72,8 +72,10 @@ never merges, never decides truth — it emits facts for the layers above.
|
|
|
72
72
|
enrolls as repo territory; a `.git` appearing inside a live files root
|
|
73
73
|
closes the file channels under it and births a repo root
|
|
74
74
|
(`repo.discovered`); a `.git` vanishing returns the ground to files
|
|
75
|
-
(`repo.removed`) and the channels are reborn. A repo's changes travel
|
|
76
|
-
one
|
|
75
|
+
(`repo.removed`) and the channels are reborn. A repo's raw changes travel
|
|
76
|
+
as one Card + Checkpoint state, never as thousands of realtime file
|
|
77
|
+
events; its registered child channels reconcile only after that state
|
|
78
|
+
settles. Enrollment names ground
|
|
77
79
|
by ADDRESS: the parent chain resolves, the last segment is inspected,
|
|
78
80
|
never traversed — an explicitly enrolled symlink enrolls as the
|
|
79
81
|
pointer itself.
|
|
@@ -114,8 +116,8 @@ never merges, never decides truth — it emits facts for the layers above.
|
|
|
114
116
|
and the edge-following enrollment operation (`edges.js`), not any
|
|
115
117
|
walk, is what turns unclaimed targets into ground. Every root kind
|
|
116
118
|
exposes its edges (`linkEdges`): files ground from detected truth,
|
|
117
|
-
repo ground from a fresh worktree census — a repo's changes travel
|
|
118
|
-
as one
|
|
119
|
+
repo ground from a fresh worktree census — a repo's raw changes travel
|
|
120
|
+
as one Card + Checkpoint state, but its edges are enrollment-policy inputs like any
|
|
119
121
|
other root's.
|
|
120
122
|
|
|
121
123
|
## Shape
|
|
@@ -179,8 +181,8 @@ never merges, never decides truth — it emits facts for the layers above.
|
|
|
179
181
|
The registration boundary above (`registration/`) is what makes
|
|
180
182
|
watched ground REGISTERED ground — a tree per top-level root, keyed
|
|
181
183
|
by permanent rootId — and owns derived link resolution across trees
|
|
182
|
-
(answering with the OWNING entity: repo
|
|
183
|
-
|
|
184
|
+
(answering with the OWNING entity: a repo boundary changes realtime
|
|
185
|
+
semantics, but an address in it resolves to its registered child).
|
|
184
186
|
|
|
185
187
|
The observer imports nothing from the rest of the runtime. Enrollment policy
|
|
186
188
|
(`shouldEnroll`) is injected; the default is the workspace inclusion rules.
|