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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/runtime/lib/harnesses.js +61 -162
  3. package/runtime/scripts/amalgm-mcp/agent-bundles/app-entries.js +124 -0
  4. package/runtime/scripts/amalgm-mcp/agent-bundles/app-files.js +160 -0
  5. package/runtime/scripts/amalgm-mcp/agent-bundles/automation-entries.js +186 -0
  6. package/runtime/scripts/amalgm-mcp/agent-bundles/bundles.js +272 -32
  7. package/runtime/scripts/amalgm-mcp/agent-bundles/rest.js +25 -8
  8. package/runtime/scripts/amalgm-mcp/automations/rest.js +21 -11
  9. package/runtime/scripts/amalgm-mcp/automations/store.js +68 -1
  10. package/runtime/scripts/amalgm-mcp/automations/tools.js +13 -4
  11. package/runtime/scripts/amalgm-mcp/events/desktop-release-runner.js +1 -0
  12. package/runtime/scripts/amalgm-mcp/lib/email-deeplinks.js +117 -0
  13. package/runtime/scripts/amalgm-mcp/lib/email-embeds.js +42 -19
  14. package/runtime/scripts/amalgm-mcp/lib/email-md.js +24 -17
  15. package/runtime/scripts/amalgm-mcp/notify/index.js +6 -2
  16. package/runtime/scripts/amalgm-mcp/state/snapshot.js +3 -3
  17. package/runtime/scripts/amalgm-mcp/tests/agent-bundles.test.js +8 -8
  18. package/runtime/scripts/amalgm-mcp/tests/bundle-entries.test.js +288 -0
  19. package/runtime/scripts/amalgm-mcp/tests/desktop-release-automation-seed.test.js +37 -1
  20. package/runtime/scripts/amalgm-mcp/tests/desktop-release-runner.test.js +22 -0
  21. package/runtime/scripts/amalgm-mcp/tests/desktop-release-server.test.js +15 -5
  22. package/runtime/scripts/amalgm-mcp/tests/email-embeds.test.js +35 -0
  23. package/runtime/scripts/amalgm-mcp/workspace/rest.js +1 -0
  24. package/runtime/scripts/chat-core/adapters/acp-client.js +156 -0
  25. package/runtime/scripts/chat-core/adapters/cursor.js +254 -0
  26. package/runtime/scripts/chat-core/adapters/pi.js +302 -0
  27. package/runtime/scripts/chat-core/auth.js +20 -3
  28. package/runtime/scripts/chat-core/chat-payload.js +2 -0
  29. package/runtime/scripts/chat-core/contract.js +56 -1
  30. package/runtime/scripts/chat-core/normalizers/cursor.js +174 -0
  31. package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +38 -0
  32. package/runtime/scripts/chat-core/normalizers/pi.js +143 -0
  33. package/runtime/scripts/chat-core/server.js +4 -0
  34. package/runtime/scripts/chat-core/tests/cursor.test.js +178 -0
  35. package/runtime/scripts/chat-core/tests/pi.test.js +182 -0
  36. package/runtime/scripts/chat-core/tooling/native-config.js +38 -0
  37. 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 BUNDLE_KIND = 'amalgm.agent.bundle';
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
- tool: 1,
16
- tool_action: 2,
17
- skill: 3,
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: rootAgentId ? [graphElementId('agent', rootAgentId)] : [],
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 createAgentBundle(agentId, options = {}) {
431
- if (!agentId) throw new Error('agent_id is required');
432
- const agents = collectAgentGraph(agentId);
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
- 'Instructions, skill text, tool descriptions, and hook commands are shared as plain text.',
454
- 'Secrets pasted into free-text instructions or files are not automatically detected.',
455
- ...(hooks > 0 ? ['This bundle includes executable hook definitions. Review commands before importing.'] : []),
456
- ...(tools.length > 0 ? ['Tools can call external services or local commands. Recipients should reconnect credentials on their own machine.'] : []),
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 || agentId,
465
- title: agents[0]?.agent?.name || 'Shared agent',
466
- description: agents[0]?.agent?.description || '',
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) throw new Error(`Unsupported bundle kind: ${bundle.kind || 'unknown'}`);
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
- if (!Array.isArray(bundle.agents) || bundle.agents.length === 0) {
576
- throw new Error('bundle must include at least one agent');
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 installAgentBundle(input, options = {}) {
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
- installAgentBundle,
915
+ createBundle,
916
+ installBundle,
677
917
  validateBundle,
678
918
  };
@@ -1,19 +1,36 @@
1
1
  'use strict';
2
2
 
3
3
  const {
4
- createAgentBundle,
5
- installAgentBundle,
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 { agent_id: agentId } = body || {};
10
- if (!agentId) return sendJson(400, { error: 'agent_id is required' });
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 = createAgentBundle(agentId);
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 agent bundle' });
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 = installAgentBundle(bundle, { authMethod });
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 agent bundle';
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
  }