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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Portable automation bundle entries.
|
|
5
|
+
*
|
|
6
|
+
* An exported entry carries only what another machine needs to recreate the
|
|
7
|
+
* automation: names, trigger shapes, and workflow source text. Machine-bound
|
|
8
|
+
* state (ids, webhook secrets/URLs, project paths, schedule clocks, run
|
|
9
|
+
* history) is stripped on export; the recipient's store mints fresh values on
|
|
10
|
+
* install via the normal createAutomation path.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
createAutomation,
|
|
15
|
+
getAutomation,
|
|
16
|
+
listAutomations,
|
|
17
|
+
} = require('../automations/store');
|
|
18
|
+
const { resolveWorkflowToolAction } = require('../automations/tool-actions');
|
|
19
|
+
|
|
20
|
+
function isObject(value) {
|
|
21
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function cloneJson(value) {
|
|
25
|
+
if (value == null) return value;
|
|
26
|
+
return JSON.parse(JSON.stringify(value));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function cleanString(value) {
|
|
30
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function portableTrigger(trigger) {
|
|
34
|
+
const base = {
|
|
35
|
+
kind: trigger.kind === 'scheduled' ? 'scheduled' : 'event',
|
|
36
|
+
name: trigger.name || '',
|
|
37
|
+
description: trigger.description || '',
|
|
38
|
+
enabled: trigger.enabled !== false,
|
|
39
|
+
};
|
|
40
|
+
if (base.kind === 'scheduled') {
|
|
41
|
+
return { ...base, schedule: cloneJson(trigger.schedule) || null };
|
|
42
|
+
}
|
|
43
|
+
// Event triggers travel as shapes only. secret and webhookUrl stay behind:
|
|
44
|
+
// the recipient's normalizeTriggerRecord mints fresh ones on install.
|
|
45
|
+
return {
|
|
46
|
+
...base,
|
|
47
|
+
source: trigger.source || '*',
|
|
48
|
+
event: trigger.event || '*',
|
|
49
|
+
sourceUrl: trigger.sourceUrl || null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function portableWorkflow(workflow) {
|
|
54
|
+
return {
|
|
55
|
+
name: workflow.name || 'Workflow',
|
|
56
|
+
description: workflow.description || '',
|
|
57
|
+
enabled: workflow.enabled !== false,
|
|
58
|
+
workflowText: workflow.workflowText || '',
|
|
59
|
+
allowlist: cloneJson(workflow.allowlist) || {},
|
|
60
|
+
limits: cloneJson(workflow.limits) || {},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Toolbox tool ids referenced by the automation's compiled workflows. These
|
|
66
|
+
* become bundled tool records; builtin/first-party refs are platform-provided
|
|
67
|
+
* and are reported separately so the bundle can list them as system deps.
|
|
68
|
+
*/
|
|
69
|
+
function workflowToolRefs(workflows) {
|
|
70
|
+
const toolboxToolIds = [];
|
|
71
|
+
const systemRefs = [];
|
|
72
|
+
const missingRefs = [];
|
|
73
|
+
for (const workflow of workflows) {
|
|
74
|
+
const cells = Array.isArray(workflow.workflowIr?.cells) ? workflow.workflowIr.cells : [];
|
|
75
|
+
for (const cell of cells) {
|
|
76
|
+
if (cell.kind !== 'tool') continue;
|
|
77
|
+
const resolved = resolveWorkflowToolAction(cell.toolId, cell.actionName);
|
|
78
|
+
const ref = `${cell.toolId}.${cell.actionName}`;
|
|
79
|
+
if (!resolved) {
|
|
80
|
+
if (!missingRefs.includes(ref)) missingRefs.push(ref);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (resolved.kind === 'toolbox') {
|
|
84
|
+
if (!toolboxToolIds.includes(resolved.toolId)) toolboxToolIds.push(resolved.toolId);
|
|
85
|
+
} else if (!systemRefs.includes(resolved.ref)) {
|
|
86
|
+
systemRefs.push(resolved.ref);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { toolboxToolIds, systemRefs, missingRefs };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function exportAutomationEntry(automationId) {
|
|
94
|
+
const automation = getAutomation(automationId);
|
|
95
|
+
if (!automation) throw new Error(`Automation not found: ${automationId}`);
|
|
96
|
+
if (automation.internal === true) {
|
|
97
|
+
throw new Error(`Internal automations cannot be shared: ${automationId}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const workflows = (automation.workflows || []).filter((workflow) => workflow?.internal !== true);
|
|
101
|
+
if (workflows.length === 0) {
|
|
102
|
+
throw new Error(`Automation has no shareable workflows: ${automationId}`);
|
|
103
|
+
}
|
|
104
|
+
const triggers = (automation.triggers || []).filter((trigger) => trigger?.internal !== true);
|
|
105
|
+
if (triggers.length === 0) {
|
|
106
|
+
throw new Error(`Automation has no shareable triggers: ${automationId}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const refs = workflowToolRefs(workflows);
|
|
110
|
+
return {
|
|
111
|
+
entry: {
|
|
112
|
+
sourceAutomationId: automation.id,
|
|
113
|
+
automation: {
|
|
114
|
+
name: automation.name || 'Automation',
|
|
115
|
+
description: automation.description || '',
|
|
116
|
+
},
|
|
117
|
+
triggers: triggers.map(portableTrigger),
|
|
118
|
+
workflows: workflows.map(portableWorkflow),
|
|
119
|
+
// Toolbox tools the workflows call — bundled alongside so the graph can
|
|
120
|
+
// draw automation → tool edges and installs carry the dependency.
|
|
121
|
+
toolIds: refs.toolboxToolIds,
|
|
122
|
+
},
|
|
123
|
+
toolboxToolIds: refs.toolboxToolIds,
|
|
124
|
+
systemRefs: refs.systemRefs,
|
|
125
|
+
missingRefs: refs.missingRefs,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function uniqueAutomationName(name) {
|
|
130
|
+
const base = cleanString(name) || 'Imported automation';
|
|
131
|
+
const existing = new Set(
|
|
132
|
+
listAutomations().map((automation) => cleanString(automation.name).toLowerCase()),
|
|
133
|
+
);
|
|
134
|
+
if (!existing.has(base.toLowerCase())) return base;
|
|
135
|
+
const imported = `${base} (imported)`;
|
|
136
|
+
if (!existing.has(imported.toLowerCase())) return imported;
|
|
137
|
+
for (let index = 2; index < 1000; index += 1) {
|
|
138
|
+
const candidate = `${base} (imported ${index})`;
|
|
139
|
+
if (!existing.has(candidate.toLowerCase())) return candidate;
|
|
140
|
+
}
|
|
141
|
+
return `${base} (${Date.now().toString(36)})`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function assertInstallableEntry(entry) {
|
|
145
|
+
if (!isObject(entry)) throw new Error('automation entry must be an object');
|
|
146
|
+
if (!isObject(entry.automation)) throw new Error('automation entry is missing its automation record');
|
|
147
|
+
if (!Array.isArray(entry.triggers) || entry.triggers.length === 0) {
|
|
148
|
+
throw new Error('automation entry must include at least one trigger');
|
|
149
|
+
}
|
|
150
|
+
if (!Array.isArray(entry.workflows) || entry.workflows.length === 0) {
|
|
151
|
+
throw new Error('automation entry must include at least one workflow');
|
|
152
|
+
}
|
|
153
|
+
for (const workflow of entry.workflows) {
|
|
154
|
+
if (!cleanString(workflow?.workflowText)) {
|
|
155
|
+
throw new Error('automation entry workflows must include workflowText');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Install a portable automation entry as a new, active local automation.
|
|
162
|
+
* Workflow text recompiles through createAutomation, event triggers get a
|
|
163
|
+
* fresh webhook secret/URL from the local store, and everything installs
|
|
164
|
+
* enabled — a shared automation starts active on the receiving machine.
|
|
165
|
+
*/
|
|
166
|
+
function installAutomationEntry(entry, options = {}) {
|
|
167
|
+
assertInstallableEntry(entry);
|
|
168
|
+
return createAutomation({
|
|
169
|
+
name: uniqueAutomationName(entry.automation.name),
|
|
170
|
+
description: entry.automation.description || '',
|
|
171
|
+
enabled: true,
|
|
172
|
+
triggers: entry.triggers.map((trigger) => ({
|
|
173
|
+
...portableTrigger(isObject(trigger) ? trigger : {}),
|
|
174
|
+
enabled: true,
|
|
175
|
+
})),
|
|
176
|
+
workflows: entry.workflows.map((workflow) => ({
|
|
177
|
+
...portableWorkflow(isObject(workflow) ? workflow : {}),
|
|
178
|
+
enabled: true,
|
|
179
|
+
})),
|
|
180
|
+
}, { source: options.source || 'agent-bundles:install' });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
exportAutomationEntry,
|
|
185
|
+
installAutomationEntry,
|
|
186
|
+
};
|
|
@@ -7,14 +7,22 @@ const { normalizeAgentConfig } = require('../agent-config/schema');
|
|
|
7
7
|
const { createAgent, getAllAgentsWithBuiltins, resolveAgent } = require('../agents/store');
|
|
8
8
|
const { hydrateConfigSkills } = require('../skills/store');
|
|
9
9
|
const { defaultToolboxService } = require('../toolbox/service');
|
|
10
|
-
|
|
11
|
-
const
|
|
10
|
+
const { exportAutomationEntry, installAutomationEntry } = require('./automation-entries');
|
|
11
|
+
const { exportAppEntry, installAppEntry } = require('./app-entries');
|
|
12
|
+
|
|
13
|
+
// Agent-only bundles keep the legacy kind so older runtimes can still install
|
|
14
|
+
// them; bundles that carry automations or apps use the general kind so older
|
|
15
|
+
// runtimes reject them outright instead of silently dropping primitives.
|
|
16
|
+
const BUNDLE_KIND = 'amalgm.bundle';
|
|
17
|
+
const LEGACY_AGENT_BUNDLE_KIND = 'amalgm.agent.bundle';
|
|
12
18
|
const BUNDLE_SCHEMA_VERSION = 1;
|
|
13
19
|
const GRAPH_ELEMENT_ORDER = {
|
|
14
20
|
agent: 0,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
automation: 1,
|
|
22
|
+
app: 2,
|
|
23
|
+
tool: 3,
|
|
24
|
+
tool_action: 4,
|
|
25
|
+
skill: 5,
|
|
18
26
|
};
|
|
19
27
|
|
|
20
28
|
const SECRET_KEY_PATTERN = /(api[_-]?key|authorization|bearer|cookie|password|passwd|secret|token|credential|private[_-]?key)/i;
|
|
@@ -196,6 +204,37 @@ function collectSkillRecords(bundleAgents) {
|
|
|
196
204
|
return [...byKey.values()];
|
|
197
205
|
}
|
|
198
206
|
|
|
207
|
+
function automationEntryId(entry) {
|
|
208
|
+
return isObject(entry) ? cleanEntryId(entry.sourceAutomationId) : '';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function appEntryId(entry) {
|
|
212
|
+
return isObject(entry) ? cleanEntryId(entry.sourceAppId) : '';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function cleanEntryId(value) {
|
|
216
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* App elements carry the portable record and a file manifest; file contents
|
|
221
|
+
* stay in bundle.apps[] so the graph remains cheap to render.
|
|
222
|
+
*/
|
|
223
|
+
function appElementData(entry) {
|
|
224
|
+
return {
|
|
225
|
+
sourceAppId: appEntryId(entry),
|
|
226
|
+
app: cloneJson(entry.app || {}),
|
|
227
|
+
files: (Array.isArray(entry.files) ? entry.files : []).map((file) => ({
|
|
228
|
+
path: file.path,
|
|
229
|
+
size: file.size,
|
|
230
|
+
encoding: file.encoding,
|
|
231
|
+
...(file.executable ? { executable: true } : {}),
|
|
232
|
+
})),
|
|
233
|
+
totalBytes: entry.totalBytes || 0,
|
|
234
|
+
skipped: cloneJson(entry.skipped || []),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
199
238
|
function buildFlatAgentBundleGraph(bundle) {
|
|
200
239
|
const elements = new Map();
|
|
201
240
|
const edges = new Map();
|
|
@@ -220,6 +259,28 @@ function buildFlatAgentBundleGraph(bundle) {
|
|
|
220
259
|
});
|
|
221
260
|
}
|
|
222
261
|
|
|
262
|
+
for (const entry of bundle.automations || []) {
|
|
263
|
+
const currentAutomationId = automationEntryId(entry);
|
|
264
|
+
if (!currentAutomationId) continue;
|
|
265
|
+
pushGraphElement(elements, {
|
|
266
|
+
id: graphElementId('automation', currentAutomationId),
|
|
267
|
+
type: 'automation',
|
|
268
|
+
label: entry.automation?.name || currentAutomationId,
|
|
269
|
+
data: cloneJson(entry),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (const entry of bundle.apps || []) {
|
|
274
|
+
const currentAppId = appEntryId(entry);
|
|
275
|
+
if (!currentAppId) continue;
|
|
276
|
+
pushGraphElement(elements, {
|
|
277
|
+
id: graphElementId('app', currentAppId),
|
|
278
|
+
type: 'app',
|
|
279
|
+
label: entry.app?.name || currentAppId,
|
|
280
|
+
data: appElementData(entry),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
223
284
|
for (const skill of skills) {
|
|
224
285
|
const elementId = skillElementId(skill);
|
|
225
286
|
if (!elementId) continue;
|
|
@@ -233,6 +294,19 @@ function buildFlatAgentBundleGraph(bundle) {
|
|
|
233
294
|
|
|
234
295
|
const includedToolIds = new Set();
|
|
235
296
|
const includedActionIds = new Set();
|
|
297
|
+
|
|
298
|
+
for (const entry of bundle.automations || []) {
|
|
299
|
+
const currentAutomationId = automationEntryId(entry);
|
|
300
|
+
if (!currentAutomationId) continue;
|
|
301
|
+
for (const toolId of uniqueStrings(entry.toolIds)) {
|
|
302
|
+
includedToolIds.add(toolId);
|
|
303
|
+
pushGraphEdge(edges, {
|
|
304
|
+
from: graphElementId('automation', currentAutomationId),
|
|
305
|
+
to: graphElementId('tool', toolId),
|
|
306
|
+
kind: 'uses-tool',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
236
310
|
for (const entry of bundle.agents || []) {
|
|
237
311
|
const currentAgentId = agentEntryId(entry);
|
|
238
312
|
if (!currentAgentId) continue;
|
|
@@ -311,18 +385,46 @@ function buildFlatAgentBundleGraph(bundle) {
|
|
|
311
385
|
});
|
|
312
386
|
}
|
|
313
387
|
|
|
314
|
-
const firstAgentId = agentEntryId(bundle.agents?.[0]);
|
|
315
|
-
const rootAgentId = typeof bundle.rootAgentId === 'string' && bundle.rootAgentId.trim()
|
|
316
|
-
? bundle.rootAgentId.trim()
|
|
317
|
-
: firstAgentId;
|
|
318
|
-
|
|
319
388
|
return {
|
|
320
|
-
headIds:
|
|
389
|
+
headIds: bundleHeadIds(bundle, elements),
|
|
321
390
|
elements: [...elements.values()].sort(compareGraphElements),
|
|
322
391
|
edges: [...edges.values()].sort(compareGraphEdges),
|
|
323
392
|
};
|
|
324
393
|
}
|
|
325
394
|
|
|
395
|
+
/**
|
|
396
|
+
* Heads are the user-facing roots of the bundle. Explicit bundle.heads win;
|
|
397
|
+
* otherwise fall back to the legacy single agent root plus every automation
|
|
398
|
+
* and app entry (those are always top-level).
|
|
399
|
+
*/
|
|
400
|
+
function bundleHeadIds(bundle, elements) {
|
|
401
|
+
const headIds = [];
|
|
402
|
+
const explicitHeads = Array.isArray(bundle.heads) ? bundle.heads : [];
|
|
403
|
+
for (const head of explicitHeads) {
|
|
404
|
+
const type = cleanEntryId(head?.type);
|
|
405
|
+
const id = cleanEntryId(head?.id);
|
|
406
|
+
if (!type || !id) continue;
|
|
407
|
+
const elementId = graphElementId(type, id);
|
|
408
|
+
if (elements.has(elementId) && !headIds.includes(elementId)) headIds.push(elementId);
|
|
409
|
+
}
|
|
410
|
+
if (headIds.length > 0) return headIds;
|
|
411
|
+
|
|
412
|
+
const firstAgentId = agentEntryId(bundle.agents?.[0]);
|
|
413
|
+
const rootAgentId = cleanEntryId(bundle.rootAgentId) || firstAgentId;
|
|
414
|
+
if (rootAgentId && elements.has(graphElementId('agent', rootAgentId))) {
|
|
415
|
+
headIds.push(graphElementId('agent', rootAgentId));
|
|
416
|
+
}
|
|
417
|
+
for (const entry of bundle.automations || []) {
|
|
418
|
+
const id = automationEntryId(entry);
|
|
419
|
+
if (id) headIds.push(graphElementId('automation', id));
|
|
420
|
+
}
|
|
421
|
+
for (const entry of bundle.apps || []) {
|
|
422
|
+
const id = appEntryId(entry);
|
|
423
|
+
if (id) headIds.push(graphElementId('app', id));
|
|
424
|
+
}
|
|
425
|
+
return [...new Set(headIds)];
|
|
426
|
+
}
|
|
427
|
+
|
|
326
428
|
function assertClosedFlatBundleGraph(bundle) {
|
|
327
429
|
if (!Array.isArray(bundle.headIds) || bundle.headIds.length === 0) {
|
|
328
430
|
throw new Error('bundle graph must include at least one head');
|
|
@@ -427,18 +529,67 @@ function collectAgentGraph(rootAgentId) {
|
|
|
427
529
|
return ordered;
|
|
428
530
|
}
|
|
429
531
|
|
|
430
|
-
function
|
|
431
|
-
|
|
432
|
-
|
|
532
|
+
function bundleTitleAndDescription(agents, automations, apps) {
|
|
533
|
+
const firstAgent = agents[0]?.agent;
|
|
534
|
+
if (firstAgent) {
|
|
535
|
+
return { title: firstAgent.name || 'Shared agent', description: firstAgent.description || '' };
|
|
536
|
+
}
|
|
537
|
+
const firstAutomation = automations[0]?.automation;
|
|
538
|
+
if (firstAutomation) {
|
|
539
|
+
return { title: firstAutomation.name || 'Shared automation', description: firstAutomation.description || '' };
|
|
540
|
+
}
|
|
541
|
+
const firstApp = apps[0]?.app;
|
|
542
|
+
if (firstApp) {
|
|
543
|
+
return { title: firstApp.name || 'Shared app', description: firstApp.description || '' };
|
|
544
|
+
}
|
|
545
|
+
return { title: 'Shared bundle', description: '' };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function bundleWarnings({ hooks, tools, automations, apps, skippedAppFiles }) {
|
|
549
|
+
return [
|
|
550
|
+
'Instructions, skill text, tool descriptions, and hook commands are shared as plain text.',
|
|
551
|
+
'Secrets pasted into free-text instructions or files are not automatically detected.',
|
|
552
|
+
...(hooks > 0 ? ['This bundle includes executable hook definitions. Review commands before importing.'] : []),
|
|
553
|
+
...(tools > 0 ? ['Tools can call external services or local commands. Recipients should reconnect credentials on their own machine.'] : []),
|
|
554
|
+
...(automations > 0 ? ['This bundle includes automations. Their workflows run code and call tools on the importing machine — review workflow scripts before importing.'] : []),
|
|
555
|
+
...(apps > 0 ? ['This bundle includes apps. Their build and start commands execute on install — review commands before importing.'] : []),
|
|
556
|
+
...(skippedAppFiles > 0 ? [`${skippedAppFiles} app file${skippedAppFiles === 1 ? ' was' : 's were'} excluded from the bundle (dependencies, build outputs, env files, or size limits). Builds re-run on install.`] : []),
|
|
557
|
+
];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Build a portable bundle from any mix of top-level primitives. Each requested
|
|
562
|
+
* id becomes a head; agents pull in their full subagent graph, and agents and
|
|
563
|
+
* automations pull in the toolbox tools they reference.
|
|
564
|
+
*/
|
|
565
|
+
function createBundle(input = {}, options = {}) {
|
|
566
|
+
const agentIds = uniqueStrings(input.agentIds);
|
|
567
|
+
const automationIds = uniqueStrings(input.automationIds);
|
|
568
|
+
const appIds = uniqueStrings(input.appIds);
|
|
569
|
+
if (agentIds.length === 0 && automationIds.length === 0 && appIds.length === 0) {
|
|
570
|
+
throw new Error('At least one agent, automation, or app is required');
|
|
571
|
+
}
|
|
572
|
+
|
|
433
573
|
const requires = {
|
|
434
574
|
runtimes: [],
|
|
435
575
|
auth: [],
|
|
436
576
|
secrets: [],
|
|
437
577
|
bindings: [],
|
|
438
578
|
systemTools: [],
|
|
579
|
+
systemActions: [],
|
|
439
580
|
missingTools: [],
|
|
440
581
|
};
|
|
441
582
|
|
|
583
|
+
const agents = [];
|
|
584
|
+
const seenAgentIds = new Set();
|
|
585
|
+
for (const agentId of agentIds) {
|
|
586
|
+
for (const entry of collectAgentGraph(agentId)) {
|
|
587
|
+
if (seenAgentIds.has(entry.sourceAgentId)) continue;
|
|
588
|
+
seenAgentIds.add(entry.sourceAgentId);
|
|
589
|
+
agents.push(entry);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
442
593
|
const loadoutToolIds = [];
|
|
443
594
|
for (const entry of agents) {
|
|
444
595
|
addUnique(requires.runtimes, entry.agent.baseHarnessId);
|
|
@@ -446,25 +597,52 @@ function createAgentBundle(agentId, options = {}) {
|
|
|
446
597
|
loadoutToolIds.push(...(entry.config.loadout?.toolIds || []));
|
|
447
598
|
}
|
|
448
599
|
|
|
600
|
+
const automations = [];
|
|
601
|
+
for (const automationId of automationIds) {
|
|
602
|
+
const exported = exportAutomationEntry(automationId);
|
|
603
|
+
automations.push(exported.entry);
|
|
604
|
+
loadoutToolIds.push(...exported.toolboxToolIds);
|
|
605
|
+
for (const ref of exported.systemRefs) addUnique(requires.systemActions, ref);
|
|
606
|
+
for (const ref of exported.missingRefs) addUnique(requires.missingTools, ref);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const apps = [];
|
|
610
|
+
let skippedAppFiles = 0;
|
|
611
|
+
for (const appId of appIds) {
|
|
612
|
+
const exported = exportAppEntry(appId);
|
|
613
|
+
apps.push(exported.entry);
|
|
614
|
+
skippedAppFiles += exported.skipped.length;
|
|
615
|
+
}
|
|
616
|
+
|
|
449
617
|
const { tools, toolActions } = collectToolRecords(loadoutToolIds, requires);
|
|
450
618
|
const skills = collectSkillRecords(agents);
|
|
451
619
|
const hooks = countHooks(agents);
|
|
452
|
-
const warnings =
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
620
|
+
const warnings = bundleWarnings({
|
|
621
|
+
hooks,
|
|
622
|
+
tools: tools.length,
|
|
623
|
+
automations: automations.length,
|
|
624
|
+
apps: apps.length,
|
|
625
|
+
skippedAppFiles,
|
|
626
|
+
});
|
|
627
|
+
const { title, description } = bundleTitleAndDescription(agents, automations, apps);
|
|
628
|
+
const heads = [
|
|
629
|
+
...agentIds.map((id) => ({ type: 'agent', id })),
|
|
630
|
+
...automationIds.map((id) => ({ type: 'automation', id })),
|
|
631
|
+
...appIds.map((id) => ({ type: 'app', id })),
|
|
457
632
|
];
|
|
458
633
|
|
|
459
634
|
const bundle = withBundleGraph({
|
|
460
|
-
kind: BUNDLE_KIND,
|
|
635
|
+
kind: automations.length > 0 || apps.length > 0 ? BUNDLE_KIND : LEGACY_AGENT_BUNDLE_KIND,
|
|
461
636
|
schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
462
637
|
bundleId: options.bundleId || bundleId(),
|
|
463
638
|
createdAt: nowIso(),
|
|
464
|
-
rootAgentId: agents[0]?.sourceAgentId ||
|
|
465
|
-
|
|
466
|
-
|
|
639
|
+
rootAgentId: agents[0]?.sourceAgentId || null,
|
|
640
|
+
heads,
|
|
641
|
+
title,
|
|
642
|
+
description,
|
|
467
643
|
agents,
|
|
644
|
+
automations,
|
|
645
|
+
apps,
|
|
468
646
|
tools,
|
|
469
647
|
toolActions,
|
|
470
648
|
skills,
|
|
@@ -473,6 +651,8 @@ function createAgentBundle(agentId, options = {}) {
|
|
|
473
651
|
warnings,
|
|
474
652
|
summary: {
|
|
475
653
|
agents: agents.length,
|
|
654
|
+
automations: automations.length,
|
|
655
|
+
apps: apps.length,
|
|
476
656
|
tools: tools.length,
|
|
477
657
|
toolActions: toolActions.length,
|
|
478
658
|
skills: skills.length,
|
|
@@ -502,6 +682,22 @@ function createAgentBundle(agentId, options = {}) {
|
|
|
502
682
|
subagents: entry.config.subagents?.length || 0,
|
|
503
683
|
hooks: entry.config.hooks?.length || 0,
|
|
504
684
|
})),
|
|
685
|
+
automations: automations.map((entry) => ({
|
|
686
|
+
sourceAutomationId: entry.sourceAutomationId,
|
|
687
|
+
name: entry.automation.name,
|
|
688
|
+
description: entry.automation.description,
|
|
689
|
+
triggers: entry.triggers.length,
|
|
690
|
+
workflows: entry.workflows.length,
|
|
691
|
+
tools: entry.toolIds.length,
|
|
692
|
+
})),
|
|
693
|
+
apps: apps.map((entry) => ({
|
|
694
|
+
sourceAppId: entry.sourceAppId,
|
|
695
|
+
name: entry.app.name,
|
|
696
|
+
description: entry.app.description,
|
|
697
|
+
files: entry.files.length,
|
|
698
|
+
totalBytes: entry.totalBytes,
|
|
699
|
+
skippedFiles: entry.skipped.length,
|
|
700
|
+
})),
|
|
505
701
|
tools: tools.map((tool) => ({
|
|
506
702
|
id: tool.id,
|
|
507
703
|
name: tool.name,
|
|
@@ -514,6 +710,11 @@ function createAgentBundle(agentId, options = {}) {
|
|
|
514
710
|
};
|
|
515
711
|
}
|
|
516
712
|
|
|
713
|
+
function createAgentBundle(agentId, options = {}) {
|
|
714
|
+
if (!agentId) throw new Error('agent_id is required');
|
|
715
|
+
return createBundle({ agentIds: [agentId] }, options);
|
|
716
|
+
}
|
|
717
|
+
|
|
517
718
|
function uniqueAgentName(name) {
|
|
518
719
|
const base = String(name || 'Imported agent').trim() || 'Imported agent';
|
|
519
720
|
const existing = new Set(
|
|
@@ -568,14 +769,19 @@ function assertClosedAgentBundleGraph(bundle) {
|
|
|
568
769
|
|
|
569
770
|
function validateBundle(bundle) {
|
|
570
771
|
if (!isObject(bundle)) throw new Error('bundle must be an object');
|
|
571
|
-
if (bundle.kind !== BUNDLE_KIND
|
|
772
|
+
if (bundle.kind !== BUNDLE_KIND && bundle.kind !== LEGACY_AGENT_BUNDLE_KIND) {
|
|
773
|
+
throw new Error(`Unsupported bundle kind: ${bundle.kind || 'unknown'}`);
|
|
774
|
+
}
|
|
572
775
|
if (bundle.schemaVersion !== BUNDLE_SCHEMA_VERSION) {
|
|
573
776
|
throw new Error(`Unsupported bundle schema version: ${bundle.schemaVersion || 'unknown'}`);
|
|
574
777
|
}
|
|
575
|
-
|
|
576
|
-
|
|
778
|
+
const agents = Array.isArray(bundle.agents) ? bundle.agents : [];
|
|
779
|
+
const automations = Array.isArray(bundle.automations) ? bundle.automations : [];
|
|
780
|
+
const apps = Array.isArray(bundle.apps) ? bundle.apps : [];
|
|
781
|
+
if (agents.length === 0 && automations.length === 0 && apps.length === 0) {
|
|
782
|
+
throw new Error('bundle must include at least one agent, automation, or app');
|
|
577
783
|
}
|
|
578
|
-
assertClosedAgentBundleGraph(bundle);
|
|
784
|
+
if (agents.length > 0) assertClosedAgentBundleGraph(bundle);
|
|
579
785
|
return withBundleGraph(bundle);
|
|
580
786
|
}
|
|
581
787
|
|
|
@@ -601,14 +807,12 @@ function installTools(bundle) {
|
|
|
601
807
|
return installed;
|
|
602
808
|
}
|
|
603
809
|
|
|
604
|
-
function
|
|
605
|
-
const bundle = validateBundle(input);
|
|
606
|
-
const installedTools = installTools(bundle);
|
|
810
|
+
function installBundleAgents(bundle, options = {}) {
|
|
607
811
|
const idMap = new Map();
|
|
608
812
|
const createdAgents = [];
|
|
609
813
|
const createdAt = nowIso();
|
|
610
814
|
|
|
611
|
-
for (const entry of bundle.agents) {
|
|
815
|
+
for (const entry of Array.isArray(bundle.agents) ? bundle.agents : []) {
|
|
612
816
|
const config = normalizeAgentConfig(entry.config || {});
|
|
613
817
|
const fields = agentFieldsFromConfig(config);
|
|
614
818
|
const agent = createAgent({
|
|
@@ -658,6 +862,38 @@ function installAgentBundle(input, options = {}) {
|
|
|
658
862
|
});
|
|
659
863
|
}
|
|
660
864
|
|
|
865
|
+
return { idMap, installedAgents };
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function defaultAppsInstallRoot() {
|
|
869
|
+
const { getWorkspaceRoot } = require('../workspace/rest');
|
|
870
|
+
return getWorkspaceRoot();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Install every primitive in a validated bundle: tools first (agents and
|
|
875
|
+
* automations reference them), then agents, automations, and apps. Apps
|
|
876
|
+
* materialize into the local workspace root, build, and start.
|
|
877
|
+
*/
|
|
878
|
+
async function installBundle(input, options = {}) {
|
|
879
|
+
const bundle = validateBundle(input);
|
|
880
|
+
const installedTools = installTools(bundle);
|
|
881
|
+
const { idMap, installedAgents } = installBundleAgents(bundle, options);
|
|
882
|
+
|
|
883
|
+
const installedAutomations = [];
|
|
884
|
+
for (const entry of Array.isArray(bundle.automations) ? bundle.automations : []) {
|
|
885
|
+
installedAutomations.push(installAutomationEntry(entry, { source: 'agent-bundles:install' }));
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const installedApps = [];
|
|
889
|
+
const appEntries = Array.isArray(bundle.apps) ? bundle.apps : [];
|
|
890
|
+
if (appEntries.length > 0) {
|
|
891
|
+
const rootDir = options.appsRootDir || defaultAppsInstallRoot();
|
|
892
|
+
for (const entry of appEntries) {
|
|
893
|
+
installedApps.push(await installAppEntry(entry, { rootDir, register: options.registerApp }));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
661
897
|
return {
|
|
662
898
|
ok: true,
|
|
663
899
|
bundleId: bundle.bundleId,
|
|
@@ -665,6 +901,8 @@ function installAgentBundle(input, options = {}) {
|
|
|
665
901
|
idMap: Object.fromEntries(idMap.entries()),
|
|
666
902
|
installedAgents,
|
|
667
903
|
installedTools,
|
|
904
|
+
installedAutomations,
|
|
905
|
+
installedApps,
|
|
668
906
|
warnings: bundle.warnings || [],
|
|
669
907
|
};
|
|
670
908
|
}
|
|
@@ -672,7 +910,9 @@ function installAgentBundle(input, options = {}) {
|
|
|
672
910
|
module.exports = {
|
|
673
911
|
BUNDLE_KIND,
|
|
674
912
|
BUNDLE_SCHEMA_VERSION,
|
|
913
|
+
LEGACY_AGENT_BUNDLE_KIND,
|
|
675
914
|
createAgentBundle,
|
|
676
|
-
|
|
915
|
+
createBundle,
|
|
916
|
+
installBundle,
|
|
677
917
|
validateBundle,
|
|
678
918
|
};
|
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
createBundle,
|
|
5
|
+
installBundle,
|
|
6
6
|
} = require('./bundles');
|
|
7
7
|
|
|
8
|
+
function idList(...values) {
|
|
9
|
+
const ids = [];
|
|
10
|
+
for (const value of values) {
|
|
11
|
+
if (typeof value === 'string' && value.trim()) ids.push(value.trim());
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
for (const item of value) {
|
|
14
|
+
if (typeof item === 'string' && item.trim()) ids.push(item.trim());
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return [...new Set(ids)];
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
async function handlePreview(body, sendJson) {
|
|
9
|
-
const
|
|
10
|
-
|
|
22
|
+
const agentIds = idList(body?.agent_id, body?.agent_ids);
|
|
23
|
+
const automationIds = idList(body?.automation_id, body?.automation_ids);
|
|
24
|
+
const appIds = idList(body?.app_id, body?.app_ids);
|
|
25
|
+
if (agentIds.length === 0 && automationIds.length === 0 && appIds.length === 0) {
|
|
26
|
+
return sendJson(400, { error: 'At least one of agent_ids, automation_ids, or app_ids is required' });
|
|
27
|
+
}
|
|
11
28
|
|
|
12
29
|
try {
|
|
13
|
-
const result =
|
|
30
|
+
const result = createBundle({ agentIds, automationIds, appIds });
|
|
14
31
|
return sendJson(200, { ok: true, ...result });
|
|
15
32
|
} catch (error) {
|
|
16
|
-
return sendJson(400, { error: error.message || 'Failed to create
|
|
33
|
+
return sendJson(400, { error: error.message || 'Failed to create bundle' });
|
|
17
34
|
}
|
|
18
35
|
}
|
|
19
36
|
|
|
@@ -22,10 +39,10 @@ async function handleInstall(body, sendJson) {
|
|
|
22
39
|
if (!bundle) return sendJson(400, { error: 'bundle is required' });
|
|
23
40
|
|
|
24
41
|
try {
|
|
25
|
-
const result =
|
|
42
|
+
const result = await installBundle(bundle, { authMethod });
|
|
26
43
|
return sendJson(200, result);
|
|
27
44
|
} catch (error) {
|
|
28
|
-
const message = error.message || 'Failed to install
|
|
45
|
+
const message = error.message || 'Failed to install bundle';
|
|
29
46
|
return sendJson(message.includes('Unsupported bundle') ? 400 : 500, { error: message });
|
|
30
47
|
}
|
|
31
48
|
}
|