amalgm 0.1.135 → 0.1.136
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/package.json +1 -1
- package/runtime/lib/harnesses.js +61 -162
- package/runtime/scripts/amalgm-mcp/agent-bundles/app-entries.js +124 -0
- package/runtime/scripts/amalgm-mcp/agent-bundles/app-files.js +160 -0
- package/runtime/scripts/amalgm-mcp/agent-bundles/automation-entries.js +186 -0
- package/runtime/scripts/amalgm-mcp/agent-bundles/bundles.js +272 -32
- package/runtime/scripts/amalgm-mcp/agent-bundles/rest.js +25 -8
- package/runtime/scripts/amalgm-mcp/automations/rest.js +21 -11
- package/runtime/scripts/amalgm-mcp/automations/store.js +68 -1
- package/runtime/scripts/amalgm-mcp/automations/tools.js +13 -4
- package/runtime/scripts/amalgm-mcp/events/desktop-release-runner.js +1 -0
- package/runtime/scripts/amalgm-mcp/lib/email-deeplinks.js +117 -0
- package/runtime/scripts/amalgm-mcp/lib/email-embeds.js +42 -19
- package/runtime/scripts/amalgm-mcp/lib/email-md.js +24 -17
- package/runtime/scripts/amalgm-mcp/notify/index.js +6 -2
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +3 -3
- package/runtime/scripts/amalgm-mcp/tests/agent-bundles.test.js +8 -8
- package/runtime/scripts/amalgm-mcp/tests/bundle-entries.test.js +288 -0
- package/runtime/scripts/amalgm-mcp/tests/desktop-release-automation-seed.test.js +37 -1
- package/runtime/scripts/amalgm-mcp/tests/desktop-release-runner.test.js +22 -0
- package/runtime/scripts/amalgm-mcp/tests/desktop-release-server.test.js +15 -5
- package/runtime/scripts/amalgm-mcp/tests/email-embeds.test.js +35 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +1 -0
- package/runtime/scripts/chat-core/adapters/acp-client.js +156 -0
- package/runtime/scripts/chat-core/adapters/cursor.js +254 -0
- package/runtime/scripts/chat-core/adapters/pi.js +302 -0
- package/runtime/scripts/chat-core/auth.js +20 -3
- package/runtime/scripts/chat-core/chat-payload.js +2 -0
- package/runtime/scripts/chat-core/contract.js +56 -1
- package/runtime/scripts/chat-core/normalizers/cursor.js +174 -0
- package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +38 -0
- package/runtime/scripts/chat-core/normalizers/pi.js +143 -0
- package/runtime/scripts/chat-core/server.js +4 -0
- package/runtime/scripts/chat-core/tests/cursor.test.js +178 -0
- package/runtime/scripts/chat-core/tests/pi.test.js +182 -0
- package/runtime/scripts/chat-core/tooling/native-config.js +38 -0
- package/runtime/scripts/chat-core/usage.js +20 -0
|
@@ -8,14 +8,14 @@ const {
|
|
|
8
8
|
createAutomation,
|
|
9
9
|
deleteAutomation,
|
|
10
10
|
deleteAutomationByTriggerId,
|
|
11
|
-
getAutomation,
|
|
12
|
-
getAutomationByTriggerId,
|
|
13
11
|
getAutomationRun,
|
|
12
|
+
getPublicAutomation,
|
|
13
|
+
getPublicAutomationByTriggerId,
|
|
14
14
|
listAutomationRuns,
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
listPublicAutomations,
|
|
16
|
+
listPublicTriggers,
|
|
17
|
+
listPublicWorkflows,
|
|
17
18
|
listWorkflowCellRuns,
|
|
18
|
-
listWorkflows,
|
|
19
19
|
updateAutomation,
|
|
20
20
|
updateAutomationByTriggerId,
|
|
21
21
|
} = require('./store');
|
|
@@ -28,9 +28,9 @@ function sendError(sendJson, status, error) {
|
|
|
28
28
|
|
|
29
29
|
async function handleList(_req, sendJson) {
|
|
30
30
|
sendJson(200, {
|
|
31
|
-
automations:
|
|
32
|
-
triggers:
|
|
33
|
-
workflows:
|
|
31
|
+
automations: listPublicAutomations(),
|
|
32
|
+
triggers: listPublicTriggers(),
|
|
33
|
+
workflows: listPublicWorkflows(),
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -38,9 +38,9 @@ async function handleGet(body, sendJson) {
|
|
|
38
38
|
const automationId = body?.automation_id || body?.automationId;
|
|
39
39
|
const triggerId = body?.trigger_id || body?.triggerId;
|
|
40
40
|
const automation = automationId
|
|
41
|
-
?
|
|
41
|
+
? getPublicAutomation(automationId)
|
|
42
42
|
: triggerId
|
|
43
|
-
?
|
|
43
|
+
? getPublicAutomationByTriggerId(triggerId)
|
|
44
44
|
: null;
|
|
45
45
|
if (!automation) return sendJson(404, { error: 'Automation not found' });
|
|
46
46
|
sendJson(200, {
|
|
@@ -64,13 +64,18 @@ async function handleRuns(body, sendJson) {
|
|
|
64
64
|
if (runId) {
|
|
65
65
|
const run = getAutomationRun(runId);
|
|
66
66
|
if (!run) return sendJson(404, { error: 'Run not found' });
|
|
67
|
+
if (!getPublicAutomation(run.automationId)) {
|
|
68
|
+
return sendJson(404, { error: 'Run not found' });
|
|
69
|
+
}
|
|
67
70
|
return sendJson(200, { ok: true, run, cellRuns: listWorkflowCellRuns({ runId: run.id }) });
|
|
68
71
|
}
|
|
69
72
|
const automationId = body?.automation_id || body?.automationId;
|
|
70
73
|
if (!automationId) return sendJson(400, { error: 'automation_id or run_id is required' });
|
|
74
|
+
const automation = getPublicAutomation(automationId);
|
|
75
|
+
if (!automation) return sendJson(404, { error: 'Automation not found' });
|
|
71
76
|
const limit = Math.max(1, Math.min(Number(body?.limit) || 50, 500));
|
|
72
77
|
const offset = Math.max(0, Number(body?.offset) || 0);
|
|
73
|
-
const page = listAutomationRuns({ automationId, limit: limit + 1, offset });
|
|
78
|
+
const page = listAutomationRuns({ automationId: automation.id, limit: limit + 1, offset });
|
|
74
79
|
sendJson(200, {
|
|
75
80
|
ok: true,
|
|
76
81
|
runs: page.slice(0, limit),
|
|
@@ -110,6 +115,7 @@ async function handleUpdate(body, sendJson) {
|
|
|
110
115
|
const { automation_id, automationId, workflow, ...updates } = body || {};
|
|
111
116
|
const id = automation_id || automationId;
|
|
112
117
|
if (!id) return sendJson(400, { error: 'automation_id is required' });
|
|
118
|
+
if (!getPublicAutomation(id)) return sendJson(404, { error: 'Automation not found' });
|
|
113
119
|
const automation = updateAutomation(id, {
|
|
114
120
|
...updates,
|
|
115
121
|
...(workflow !== undefined && updates.workflowText === undefined ? { workflowText: workflow } : {}),
|
|
@@ -124,6 +130,7 @@ async function handleDelete(body, sendJson) {
|
|
|
124
130
|
try {
|
|
125
131
|
const id = body?.automation_id || body?.automationId;
|
|
126
132
|
if (!id) return sendJson(400, { error: 'automation_id is required' });
|
|
133
|
+
if (!getPublicAutomation(id)) return sendJson(404, { error: 'Automation not found' });
|
|
127
134
|
const deleted = deleteAutomation(id, { source: 'automations:rest:delete' });
|
|
128
135
|
sendJson(200, { ok: true, deleted });
|
|
129
136
|
} catch (error) {
|
|
@@ -135,6 +142,7 @@ async function handleRunNow(body, sendJson) {
|
|
|
135
142
|
try {
|
|
136
143
|
const id = body?.automation_id || body?.automationId;
|
|
137
144
|
if (!id) return sendJson(400, { error: 'automation_id is required' });
|
|
145
|
+
if (!getPublicAutomation(id)) return sendJson(404, { error: 'Automation not found' });
|
|
138
146
|
const runs = await executeAutomationById(id, {
|
|
139
147
|
id: `manual:${Date.now()}`,
|
|
140
148
|
source: 'amalgm.manual',
|
|
@@ -154,6 +162,7 @@ async function handleTriggerUpdate(body, sendJson) {
|
|
|
154
162
|
try {
|
|
155
163
|
const id = body?.trigger_id || body?.triggerId;
|
|
156
164
|
if (!id) return sendJson(400, { error: 'trigger_id is required' });
|
|
165
|
+
if (!getPublicAutomationByTriggerId(id)) return sendJson(404, { error: 'Trigger not found' });
|
|
157
166
|
const automation = updateAutomationByTriggerId(id, body || {}, { source: 'automations:rest:trigger-update' });
|
|
158
167
|
sendJson(200, { ok: true, automation, trigger: automation.triggers.find((trigger) => trigger.id === id) || null, workflow: automation.workflow });
|
|
159
168
|
} catch (error) {
|
|
@@ -165,6 +174,7 @@ async function handleTriggerDelete(body, sendJson) {
|
|
|
165
174
|
try {
|
|
166
175
|
const id = body?.trigger_id || body?.triggerId;
|
|
167
176
|
if (!id) return sendJson(400, { error: 'trigger_id is required' });
|
|
177
|
+
if (!getPublicAutomationByTriggerId(id)) return sendJson(404, { error: 'Trigger not found' });
|
|
168
178
|
const deleted = deleteAutomationByTriggerId(id, { source: 'automations:rest:trigger-delete' });
|
|
169
179
|
sendJson(200, { ok: true, deleted });
|
|
170
180
|
} catch (error) {
|
|
@@ -698,6 +698,22 @@ function composeAutomation(base, triggers, workflowOrWorkflows) {
|
|
|
698
698
|
};
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
+
function isInternalWorkflow(workflow) {
|
|
702
|
+
return workflow?.internal === true;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function isInternalTrigger(trigger) {
|
|
706
|
+
return trigger?.internal === true;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function isInternalAutomation(automation) {
|
|
710
|
+
if (!automation) return false;
|
|
711
|
+
if (automation.internal === true) return true;
|
|
712
|
+
if (Array.isArray(automation.workflows) && automation.workflows.some(isInternalWorkflow)) return true;
|
|
713
|
+
if (Array.isArray(automation.triggers) && automation.triggers.some(isInternalTrigger)) return true;
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
|
|
701
717
|
function readAutomationGraph(db = openLocalDb()) {
|
|
702
718
|
ensureAutomationsStore();
|
|
703
719
|
const automationRows = db.prepare('SELECT * FROM automation_definitions ORDER BY datetime(created_at) DESC, lower(name)').all();
|
|
@@ -748,8 +764,28 @@ function readAutomationGraph(db = openLocalDb()) {
|
|
|
748
764
|
return { automations, triggers, workflows };
|
|
749
765
|
}
|
|
750
766
|
|
|
767
|
+
function publicAutomationGraph(graph = readAutomationGraph()) {
|
|
768
|
+
const automations = (graph.automations || []).filter((automation) => !isInternalAutomation(automation));
|
|
769
|
+
const automationIds = new Set(automations.map((automation) => automation.id));
|
|
770
|
+
const triggerIds = new Set(automations.flatMap((automation) => automation.triggerIds || []));
|
|
771
|
+
const workflowIds = new Set(automations.flatMap((automation) => automation.workflowIds || []));
|
|
772
|
+
|
|
773
|
+
return {
|
|
774
|
+
automations,
|
|
775
|
+
triggers: (graph.triggers || []).filter((trigger) => (
|
|
776
|
+
automationIds.has(trigger.automationId)
|
|
777
|
+
&& triggerIds.has(trigger.id)
|
|
778
|
+
&& !isInternalTrigger(trigger)
|
|
779
|
+
)),
|
|
780
|
+
workflows: (graph.workflows || []).filter((workflow) => (
|
|
781
|
+
workflowIds.has(workflow.id)
|
|
782
|
+
&& !isInternalWorkflow(workflow)
|
|
783
|
+
)),
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
751
787
|
function publishResources(source = 'automations:save') {
|
|
752
|
-
const resources = readAutomationGraph();
|
|
788
|
+
const resources = publicAutomationGraph(readAutomationGraph());
|
|
753
789
|
appendStateEvent({ resource: 'automations', op: 'replace', value: resources.automations, source });
|
|
754
790
|
appendStateEvent({ resource: 'triggers', op: 'replace', value: resources.triggers, source });
|
|
755
791
|
appendStateEvent({ resource: 'workflows', op: 'replace', value: resources.workflows, source });
|
|
@@ -1012,18 +1048,44 @@ function listAutomations() {
|
|
|
1012
1048
|
return readAutomationGraph().automations;
|
|
1013
1049
|
}
|
|
1014
1050
|
|
|
1051
|
+
function listPublicAutomations() {
|
|
1052
|
+
return publicAutomationGraph().automations;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1015
1055
|
function listTriggers() {
|
|
1016
1056
|
return readAutomationGraph().triggers;
|
|
1017
1057
|
}
|
|
1018
1058
|
|
|
1059
|
+
function listPublicTriggers() {
|
|
1060
|
+
return publicAutomationGraph().triggers;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1019
1063
|
function listWorkflows() {
|
|
1020
1064
|
return readAutomationGraph().workflows;
|
|
1021
1065
|
}
|
|
1022
1066
|
|
|
1067
|
+
function listPublicWorkflows() {
|
|
1068
|
+
return publicAutomationGraph().workflows;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1023
1071
|
function listEventTriggersForIngress() {
|
|
1024
1072
|
return listTriggers().filter((trigger) => trigger.kind === 'event' && trigger.enabled !== false);
|
|
1025
1073
|
}
|
|
1026
1074
|
|
|
1075
|
+
function getPublicAutomation(automationId) {
|
|
1076
|
+
const id = cleanString(automationId);
|
|
1077
|
+
if (!id) return null;
|
|
1078
|
+
return listPublicAutomations().find((automation) => automation.id === id) || null;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function getPublicAutomationByTriggerId(triggerId) {
|
|
1082
|
+
const id = cleanString(triggerId);
|
|
1083
|
+
if (!id) return null;
|
|
1084
|
+
return listPublicAutomations().find((automation) => (
|
|
1085
|
+
automation.triggers.some((trigger) => trigger.id === id)
|
|
1086
|
+
)) || null;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1027
1089
|
function markTriggerFired(triggerId, options = {}) {
|
|
1028
1090
|
ensureAutomationsStore();
|
|
1029
1091
|
const automation = getAutomationByTriggerId(triggerId);
|
|
@@ -1598,10 +1660,15 @@ module.exports = {
|
|
|
1598
1660
|
getAutomation,
|
|
1599
1661
|
getAutomationByTriggerId,
|
|
1600
1662
|
getAutomationRun,
|
|
1663
|
+
getPublicAutomation,
|
|
1664
|
+
getPublicAutomationByTriggerId,
|
|
1601
1665
|
getSchedulerHeartbeat,
|
|
1602
1666
|
listAutomationRuns,
|
|
1603
1667
|
listAutomations,
|
|
1604
1668
|
listEventTriggersForIngress,
|
|
1669
|
+
listPublicAutomations,
|
|
1670
|
+
listPublicTriggers,
|
|
1671
|
+
listPublicWorkflows,
|
|
1605
1672
|
listTriggers,
|
|
1606
1673
|
listWorkflowCellRuns,
|
|
1607
1674
|
listWorkflows,
|
|
@@ -4,9 +4,9 @@ const { textResult, errorResult } = require('../lib/tool-result');
|
|
|
4
4
|
const {
|
|
5
5
|
createAutomation,
|
|
6
6
|
deleteAutomation,
|
|
7
|
-
|
|
7
|
+
getPublicAutomation,
|
|
8
8
|
listAutomationRuns,
|
|
9
|
-
|
|
9
|
+
listPublicAutomations,
|
|
10
10
|
updateAutomation,
|
|
11
11
|
} = require('./store');
|
|
12
12
|
const { executeAutomationById } = require('./runner');
|
|
@@ -312,7 +312,7 @@ module.exports = [
|
|
|
312
312
|
},
|
|
313
313
|
async handler({ enabled_only, include_workflow_text, verbose } = {}) {
|
|
314
314
|
try {
|
|
315
|
-
let automations =
|
|
315
|
+
let automations = listPublicAutomations();
|
|
316
316
|
if (enabled_only) automations = automations.filter((automation) => automation.enabled !== false);
|
|
317
317
|
const includeWorkflowText = parseBoolean(include_workflow_text, false);
|
|
318
318
|
if (automations.length === 0) return textResult('No automations found.');
|
|
@@ -341,7 +341,7 @@ module.exports = [
|
|
|
341
341
|
},
|
|
342
342
|
async handler({ automation_id, include_runs, verbose } = {}) {
|
|
343
343
|
try {
|
|
344
|
-
const automation =
|
|
344
|
+
const automation = getPublicAutomation(automation_id);
|
|
345
345
|
if (!automation) return errorResult(`Automation not found: ${automation_id}`);
|
|
346
346
|
const runs = include_runs === false ? undefined : listAutomationRuns({ automationId: automation.id, limit: 20 });
|
|
347
347
|
return textResult(JSON.stringify({
|
|
@@ -397,6 +397,9 @@ module.exports = [
|
|
|
397
397
|
},
|
|
398
398
|
async handler({ automation_id, workflow, verbose, ...updates } = {}) {
|
|
399
399
|
try {
|
|
400
|
+
if (!getPublicAutomation(automation_id)) {
|
|
401
|
+
return errorResult(`Automation not found: ${automation_id}`);
|
|
402
|
+
}
|
|
400
403
|
const automation = updateAutomation(automation_id, {
|
|
401
404
|
...updates,
|
|
402
405
|
...(workflow !== undefined && updates.workflowText === undefined ? { workflowText: workflow } : {}),
|
|
@@ -419,6 +422,9 @@ module.exports = [
|
|
|
419
422
|
},
|
|
420
423
|
async handler({ automation_id } = {}) {
|
|
421
424
|
try {
|
|
425
|
+
if (!getPublicAutomation(automation_id)) {
|
|
426
|
+
return errorResult(`Automation not found: ${automation_id}`);
|
|
427
|
+
}
|
|
422
428
|
const deleted = deleteAutomation(automation_id, { source: 'automations_delete' });
|
|
423
429
|
return textResult(`Automation "${deleted.name}" deleted.`);
|
|
424
430
|
} catch (error) {
|
|
@@ -441,6 +447,9 @@ module.exports = [
|
|
|
441
447
|
},
|
|
442
448
|
async handler({ automation_id, trigger_id, payload, verbose } = {}) {
|
|
443
449
|
try {
|
|
450
|
+
if (!getPublicAutomation(automation_id)) {
|
|
451
|
+
return errorResult(`Automation not found: ${automation_id}`);
|
|
452
|
+
}
|
|
444
453
|
const runs = await executeAutomationById(automation_id, {
|
|
445
454
|
id: `manual:${Date.now()}`,
|
|
446
455
|
source: 'amalgm.manual',
|
|
@@ -582,6 +582,7 @@ function envForBuild(
|
|
|
582
582
|
...baseEnv,
|
|
583
583
|
DESKTOP_RELEASE_LANE: request.lane,
|
|
584
584
|
DESKTOP_RELEASE_CHANNEL: request.lane === 'preview' ? 'preview' : 'latest',
|
|
585
|
+
AMALGM_DESKTOP_THIN: cleanString(baseEnv.AMALGM_DESKTOP_THIN) || '1',
|
|
585
586
|
AMALGM_RUNTIME_LABEL: request.lane === 'preview' ? 'preview' : 'main',
|
|
586
587
|
AMALGM_BRANCH: request.lane,
|
|
587
588
|
ELECTRON_VERSION: version,
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_APP_ORIGIN = 'https://amalgm.ai';
|
|
4
|
+
|
|
5
|
+
function normalizeAppOrigin(value) {
|
|
6
|
+
const raw = String(value || '').trim();
|
|
7
|
+
if (!raw) return DEFAULT_APP_ORIGIN;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const candidate = raw.includes('://') ? raw : `https://${raw}`;
|
|
11
|
+
const url = new URL(candidate);
|
|
12
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') return DEFAULT_APP_ORIGIN;
|
|
13
|
+
if (url.hostname === 'app.amalgm.ai' || url.hostname === 'www.amalgm.ai') {
|
|
14
|
+
url.hostname = 'amalgm.ai';
|
|
15
|
+
}
|
|
16
|
+
url.pathname = '';
|
|
17
|
+
url.search = '';
|
|
18
|
+
url.hash = '';
|
|
19
|
+
return url.toString().replace(/\/$/, '');
|
|
20
|
+
} catch {
|
|
21
|
+
return DEFAULT_APP_ORIGIN;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveAppOrigin(explicitOrigin) {
|
|
26
|
+
return normalizeAppOrigin(
|
|
27
|
+
explicitOrigin
|
|
28
|
+
|| process.env.AMALGM_APP_URL
|
|
29
|
+
|| process.env.AMALGM_WEB_URL
|
|
30
|
+
|| process.env.NEXT_PUBLIC_APP_URL
|
|
31
|
+
|| process.env.NEXT_PUBLIC_SITE_URL
|
|
32
|
+
|| DEFAULT_APP_ORIGIN,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeEmailLink(value, appOrigin) {
|
|
37
|
+
const raw = String(value || '').trim();
|
|
38
|
+
if (!raw) return null;
|
|
39
|
+
|
|
40
|
+
const origin = resolveAppOrigin(appOrigin);
|
|
41
|
+
if (raw.startsWith('/')) return new URL(raw, `${origin}/`).toString();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const url = new URL(raw);
|
|
45
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') return null;
|
|
46
|
+
return url.toString();
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildAbsoluteAppUrl(pathname, appOrigin) {
|
|
53
|
+
return new URL(pathname, `${resolveAppOrigin(appOrigin)}/`).toString();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildSessionDeepLink(sessionId, appOrigin) {
|
|
57
|
+
const value = String(sessionId || '').trim();
|
|
58
|
+
if (!value) return null;
|
|
59
|
+
return buildAbsoluteAppUrl(`/session/${encodeURIComponent(value)}`, appOrigin);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildEntityDeepLink(type, id, options = {}) {
|
|
63
|
+
const normalizedType = String(type || '').trim().toLowerCase();
|
|
64
|
+
const normalizedId = String(id || '').trim();
|
|
65
|
+
const fallback = normalizeEmailLink(options.defaultOpenUrl, options.appOrigin) || resolveAppOrigin(options.appOrigin);
|
|
66
|
+
|
|
67
|
+
switch (normalizedType) {
|
|
68
|
+
case 'chat':
|
|
69
|
+
case 'conversation':
|
|
70
|
+
return normalizedId
|
|
71
|
+
? buildAbsoluteAppUrl(`/session/${encodeURIComponent(normalizedId)}`, options.appOrigin)
|
|
72
|
+
: fallback;
|
|
73
|
+
case 'app':
|
|
74
|
+
case 'artifact':
|
|
75
|
+
case 'service':
|
|
76
|
+
return normalizedId
|
|
77
|
+
? buildAbsoluteAppUrl(`/apps/${encodeURIComponent(normalizedId)}`, options.appOrigin)
|
|
78
|
+
: fallback;
|
|
79
|
+
case 'automation':
|
|
80
|
+
case 'task':
|
|
81
|
+
return normalizedId
|
|
82
|
+
? buildAbsoluteAppUrl(`/tab?kind=tasks&view=task&id=${encodeURIComponent(normalizedId)}`, options.appOrigin)
|
|
83
|
+
: fallback;
|
|
84
|
+
case 'event':
|
|
85
|
+
case 'trigger':
|
|
86
|
+
return normalizedId
|
|
87
|
+
? buildAbsoluteAppUrl(`/tab?kind=tasks&view=event&id=${encodeURIComponent(normalizedId)}`, options.appOrigin)
|
|
88
|
+
: fallback;
|
|
89
|
+
case 'preview':
|
|
90
|
+
return normalizedId
|
|
91
|
+
? buildAbsoluteAppUrl(`/tab?kind=preview&port=${encodeURIComponent(normalizedId)}`, options.appOrigin)
|
|
92
|
+
: fallback;
|
|
93
|
+
default:
|
|
94
|
+
return fallback;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function buildEmailRenderContext(options = {}) {
|
|
99
|
+
const appOrigin = resolveAppOrigin(options.appOrigin);
|
|
100
|
+
const detailsUrl = normalizeEmailLink(options.detailsUrl || options.link, appOrigin);
|
|
101
|
+
const sessionUrl = normalizeEmailLink(options.sessionUrl, appOrigin) || buildSessionDeepLink(options.sessionId, appOrigin);
|
|
102
|
+
const explicitDefaultOpenUrl = normalizeEmailLink(options.defaultOpenUrl, appOrigin);
|
|
103
|
+
return {
|
|
104
|
+
appOrigin,
|
|
105
|
+
detailsUrl,
|
|
106
|
+
sessionUrl,
|
|
107
|
+
defaultOpenUrl: explicitDefaultOpenUrl || detailsUrl || sessionUrl || appOrigin,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
buildEmailRenderContext,
|
|
113
|
+
buildEntityDeepLink,
|
|
114
|
+
buildSessionDeepLink,
|
|
115
|
+
normalizeEmailLink,
|
|
116
|
+
resolveAppOrigin,
|
|
117
|
+
};
|
|
@@ -27,6 +27,7 @@ const COLOR = {
|
|
|
27
27
|
|
|
28
28
|
const FILE_ICON_REGISTRY = require('./file-icon-registry.json');
|
|
29
29
|
const { iconContentId } = require('./email-icons');
|
|
30
|
+
const { buildEntityDeepLink } = require('./email-deeplinks');
|
|
30
31
|
|
|
31
32
|
const EXTENSION_MIME = {
|
|
32
33
|
// Images
|
|
@@ -297,6 +298,10 @@ function renderImageEmbed(alt, url, href) {
|
|
|
297
298
|
: img;
|
|
298
299
|
}
|
|
299
300
|
|
|
301
|
+
function defaultOpenHref(renderContext) {
|
|
302
|
+
return (renderContext && renderContext.defaultOpenUrl) || 'https://amalgm.ai';
|
|
303
|
+
}
|
|
304
|
+
|
|
300
305
|
const PLAY_PILL =
|
|
301
306
|
`<span style="display:inline-block;background:${COLOR.bgChip};border:1px solid ${COLOR.pillBorder};border-radius:999px;padding:10px 20px;font-size:14px;color:${COLOR.text}">▶ Play video</span>`;
|
|
302
307
|
|
|
@@ -340,19 +345,20 @@ function renderVideoEmbed(label, url, poster) {
|
|
|
340
345
|
* available, and the file itself in the mail client's attachment strip —
|
|
341
346
|
* Gmail gives attached videos a playable preview natively.
|
|
342
347
|
*/
|
|
343
|
-
function renderAttachedVideoCard(label, poster) {
|
|
348
|
+
function renderAttachedVideoCard(label, poster, renderContext = {}) {
|
|
344
349
|
const identity = resolveFileEmbedIdentity({ url: `attached:${label || ''}`, fallback: label || 'Video' });
|
|
345
350
|
const name = identity.label;
|
|
346
351
|
const posterSrc = poster && isEmbeddableImageSrc(poster) ? poster : null;
|
|
352
|
+
const openHref = defaultOpenHref(renderContext);
|
|
347
353
|
// Poster is the animating GIF in Gmail; the real video rides in the
|
|
348
354
|
// attachment strip. No in-body note — the header + strip carry it.
|
|
349
355
|
// Poster <img> wrapped in a link so Gmail doesn't slap its save/download
|
|
350
356
|
// hover overlay on it (it treats linked images as clickable, not savable).
|
|
351
357
|
const body = posterSrc
|
|
352
|
-
? `<a href="
|
|
358
|
+
? `<a href="${escapeAttr(openHref)}" target="_blank" style="text-decoration:none;display:block"><img src="${escapeAttr(posterSrc)}" alt="${escapeAttr(name)}" style="display:block;width:100%;height:auto;background:${COLOR.bg}"></a>`
|
|
353
359
|
: `<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr>` +
|
|
354
360
|
`<td align="center" style="padding:44px 16px;background:${COLOR.bg}">${PLAY_PILL}</td></tr></table>`;
|
|
355
|
-
return embedShell({ iconDescriptor: resolveFileIconDescriptor(name), name, action: { href:
|
|
361
|
+
return embedShell({ iconDescriptor: resolveFileIconDescriptor(name), name, action: { href: openHref }, body });
|
|
356
362
|
}
|
|
357
363
|
|
|
358
364
|
/**
|
|
@@ -360,11 +366,15 @@ function renderAttachedVideoCard(label, poster) {
|
|
|
360
366
|
* cannot safely preview arbitrary local documents, so the chrome carries the
|
|
361
367
|
* identity and open affordance without inventing a second email-only file row.
|
|
362
368
|
*/
|
|
363
|
-
function renderFileCardEmbed(label, url) {
|
|
369
|
+
function renderFileCardEmbed(label, url, renderContext = {}) {
|
|
364
370
|
const identity = resolveFileEmbedIdentity({ url, fallback: label || 'File' });
|
|
365
371
|
const name = identity.label;
|
|
366
372
|
const http = isHttpUrl(url);
|
|
367
|
-
return embedShell({
|
|
373
|
+
return embedShell({
|
|
374
|
+
iconDescriptor: resolveFileIconDescriptor(name),
|
|
375
|
+
name,
|
|
376
|
+
action: { href: http ? url : defaultOpenHref(renderContext) },
|
|
377
|
+
});
|
|
368
378
|
}
|
|
369
379
|
|
|
370
380
|
/**
|
|
@@ -378,9 +388,9 @@ function renderFileCardEmbed(label, url) {
|
|
|
378
388
|
* Local sources that could not be attached (amalgm:// refs or filesystem
|
|
379
389
|
* paths) fall back to the file card.
|
|
380
390
|
*/
|
|
381
|
-
function renderMediaEmbed(alt, url, poster) {
|
|
391
|
+
function renderMediaEmbed(alt, url, poster, renderContext = {}) {
|
|
382
392
|
if (isCidUrl(url)) {
|
|
383
|
-
return renderImageEmbed(alt, url,
|
|
393
|
+
return renderImageEmbed(alt, url, defaultOpenHref(renderContext));
|
|
384
394
|
}
|
|
385
395
|
const attachedMatch = url.match(/^attached:(.*)$/i);
|
|
386
396
|
if (attachedMatch) {
|
|
@@ -388,7 +398,7 @@ function renderMediaEmbed(alt, url, poster) {
|
|
|
388
398
|
try {
|
|
389
399
|
name = decodeURIComponent(name);
|
|
390
400
|
} catch {}
|
|
391
|
-
return renderAttachedVideoCard(name, poster);
|
|
401
|
+
return renderAttachedVideoCard(name, poster, renderContext);
|
|
392
402
|
}
|
|
393
403
|
const amalgmMatch = url.match(/^amalgm:\/\/file\/([^\s)?"]+)/);
|
|
394
404
|
if (amalgmMatch) {
|
|
@@ -396,14 +406,14 @@ function renderMediaEmbed(alt, url, poster) {
|
|
|
396
406
|
try {
|
|
397
407
|
decoded = decodeURIComponent(decoded);
|
|
398
408
|
} catch {}
|
|
399
|
-
return renderFileCardEmbed(alt || mediaNameOf(decoded), decoded);
|
|
409
|
+
return renderFileCardEmbed(alt || mediaNameOf(decoded), decoded, renderContext);
|
|
400
410
|
}
|
|
401
411
|
if (!isHttpUrl(url)) {
|
|
402
|
-
return renderFileCardEmbed(alt || mediaNameOf(url), url);
|
|
412
|
+
return renderFileCardEmbed(alt || mediaNameOf(url), url, renderContext);
|
|
403
413
|
}
|
|
404
414
|
const kind = mediaKindOf(url);
|
|
405
415
|
if (kind === 'video') return renderVideoEmbed(alt, url, poster);
|
|
406
|
-
if (kind === 'audio' || kind === 'document') return renderFileCardEmbed(alt, url);
|
|
416
|
+
if (kind === 'audio' || kind === 'document') return renderFileCardEmbed(alt, url, renderContext);
|
|
407
417
|
// Images and extension-less URLs keep vanilla image semantics.
|
|
408
418
|
return renderImageEmbed(alt, url);
|
|
409
419
|
}
|
|
@@ -431,11 +441,12 @@ function entityTypeLabel(type) {
|
|
|
431
441
|
* Inline tile — the email-safe form of ChatRow: one black rounded row with an
|
|
432
442
|
* icon and title. No email-only type eyebrow.
|
|
433
443
|
*/
|
|
434
|
-
function renderEntityTile(label, type) {
|
|
444
|
+
function renderEntityTile(label, type, id, renderContext = {}) {
|
|
435
445
|
const safeLabel = escapeText(label || entityTypeLabel(type));
|
|
436
446
|
const iconDescriptor = resolveEntityIconDescriptor(type);
|
|
447
|
+
const href = buildEntityDeepLink(type, id, renderContext);
|
|
437
448
|
return (
|
|
438
|
-
`<a href="
|
|
449
|
+
`<a href="${escapeAttr(href)}" target="_blank" style="text-decoration:none;display:inline-block;background:${COLOR.bg};border:1px solid ${COLOR.border};border-radius:12px;padding:8px 12px;margin:2px 0;vertical-align:middle">` +
|
|
439
450
|
`<table role="presentation" cellpadding="0" cellspacing="0" style="border-collapse:collapse"><tr>` +
|
|
440
451
|
`<td style="padding:0 10px 0 0;vertical-align:middle;line-height:0">${iconHtml(iconDescriptor)}</td>` +
|
|
441
452
|
`<td style="padding:0;vertical-align:middle;font-size:14px;line-height:18px;color:${COLOR.text};font-weight:650">${safeLabel}</td>` +
|
|
@@ -449,13 +460,25 @@ function renderEntityTile(label, type) {
|
|
|
449
460
|
* the live app, so it's the EmbedChrome card with the entity identity and a
|
|
450
461
|
* prominent open action.
|
|
451
462
|
*/
|
|
452
|
-
function renderEntityExpanded(label, type) {
|
|
463
|
+
function renderEntityExpanded(label, type, id, renderContext = {}) {
|
|
453
464
|
const display = label || entityTypeLabel(type);
|
|
454
|
-
return embedShell({
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
465
|
+
return embedShell({
|
|
466
|
+
iconDescriptor: resolveEntityIconDescriptor(type),
|
|
467
|
+
name: display,
|
|
468
|
+
action: { href: buildEntityDeepLink(type, id, renderContext) },
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function renderEntityEmbed(mode, type, label, idOrRenderContext, maybeRenderContext) {
|
|
473
|
+
const id = typeof idOrRenderContext === 'string' ? idOrRenderContext : '';
|
|
474
|
+
const renderContext = (
|
|
475
|
+
idOrRenderContext && typeof idOrRenderContext === 'object' && !Array.isArray(idOrRenderContext)
|
|
476
|
+
? idOrRenderContext
|
|
477
|
+
: maybeRenderContext
|
|
478
|
+
) || {};
|
|
479
|
+
return mode === 'full'
|
|
480
|
+
? renderEntityExpanded(label, type, id, renderContext)
|
|
481
|
+
: renderEntityTile(label, type, id, renderContext);
|
|
459
482
|
}
|
|
460
483
|
|
|
461
484
|
module.exports = {
|