amalgm 0.1.51 → 0.1.53

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 (71) hide show
  1. package/lib/tunnel-events.js +48 -23
  2. package/package.json +2 -2
  3. package/runtime/lib/harnesses.js +12 -4
  4. package/runtime/scripts/amalgm-mcp/agents/store.js +5 -5
  5. package/runtime/scripts/amalgm-mcp/{artifacts → apps}/advertise.js +39 -24
  6. package/runtime/scripts/amalgm-mcp/apps/rest.js +144 -0
  7. package/runtime/scripts/amalgm-mcp/apps/store.js +171 -0
  8. package/runtime/scripts/amalgm-mcp/apps/supervisor.js +439 -0
  9. package/runtime/scripts/amalgm-mcp/apps/tools.js +176 -0
  10. package/runtime/scripts/amalgm-mcp/automations/cell-references.js +237 -0
  11. package/runtime/scripts/amalgm-mcp/automations/context.js +41 -0
  12. package/runtime/scripts/amalgm-mcp/automations/rest.js +148 -0
  13. package/runtime/scripts/amalgm-mcp/automations/runner.js +613 -0
  14. package/runtime/scripts/amalgm-mcp/automations/scheduler.js +90 -0
  15. package/runtime/scripts/amalgm-mcp/automations/store.js +1125 -0
  16. package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +177 -0
  17. package/runtime/scripts/amalgm-mcp/automations/tools.js +418 -0
  18. package/runtime/scripts/amalgm-mcp/automations/validator.js +225 -0
  19. package/runtime/scripts/amalgm-mcp/browser/agent-browser.js +547 -0
  20. package/runtime/scripts/amalgm-mcp/browser/electron-bridge.js +222 -0
  21. package/runtime/scripts/amalgm-mcp/browser/page.js +13 -631
  22. package/runtime/scripts/amalgm-mcp/browser/tools.js +9 -7
  23. package/runtime/scripts/amalgm-mcp/config.js +33 -48
  24. package/runtime/scripts/amalgm-mcp/deps.js +1 -31
  25. package/runtime/scripts/amalgm-mcp/events/ingress.js +50 -42
  26. package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +169 -0
  27. package/runtime/scripts/amalgm-mcp/events/matcher.js +45 -14
  28. package/runtime/scripts/amalgm-mcp/events/store.js +106 -57
  29. package/runtime/scripts/amalgm-mcp/index.js +12 -14
  30. package/runtime/scripts/amalgm-mcp/lib/prefs.js +229 -65
  31. package/runtime/scripts/amalgm-mcp/lib/tool-result.js +13 -27
  32. package/runtime/scripts/amalgm-mcp/server/core-tools.js +2 -3
  33. package/runtime/scripts/amalgm-mcp/server/http.js +106 -56
  34. package/runtime/scripts/amalgm-mcp/slack/inbound.js +1 -1
  35. package/runtime/scripts/amalgm-mcp/state/db.js +119 -0
  36. package/runtime/scripts/amalgm-mcp/state/snapshot.js +16 -3
  37. package/runtime/scripts/amalgm-mcp/tasks/executor.js +1 -1
  38. package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +348 -0
  39. package/runtime/scripts/amalgm-mcp/tests/events-matcher.test.js +23 -0
  40. package/runtime/scripts/amalgm-mcp/tests/workflows-store-runner.test.js +67 -0
  41. package/runtime/scripts/amalgm-mcp/toolbox/tools.js +16 -3
  42. package/runtime/scripts/amalgm-mcp/workflows/compiler.js +222 -0
  43. package/runtime/scripts/amalgm-mcp/workflows/runner.js +593 -0
  44. package/runtime/scripts/amalgm-mcp/workflows/store.js +237 -0
  45. package/runtime/scripts/chat-core/adapters/claude.js +2 -1
  46. package/runtime/scripts/chat-core/auth.js +82 -12
  47. package/runtime/scripts/chat-core/contract.js +5 -1
  48. package/runtime/scripts/chat-core/engine.js +103 -62
  49. package/runtime/scripts/chat-core/event-schema.js +8 -0
  50. package/runtime/scripts/chat-core/events.js +5 -0
  51. package/runtime/scripts/chat-core/normalizers/codex.js +13 -1
  52. package/runtime/scripts/chat-core/parts.js +21 -6
  53. package/runtime/scripts/chat-core/sse.js +3 -0
  54. package/runtime/scripts/chat-core/tests/auth.test.js +84 -6
  55. package/runtime/scripts/chat-core/tests/engine.test.js +312 -0
  56. package/runtime/scripts/chat-core/tests/native-config.test.js +23 -0
  57. package/runtime/scripts/chat-core/tool-shape.js +4 -4
  58. package/runtime/scripts/chat-core/tooling/active-memory.js +5 -4
  59. package/runtime/scripts/chat-core/tooling/native-binaries.js +34 -9
  60. package/runtime/scripts/chat-core/tooling/native-config.js +34 -3
  61. package/runtime/scripts/local-gateway.js +34 -27
  62. package/runtime/scripts/platform-context.txt +76 -94
  63. package/runtime/scripts/amalgm-mcp/artifacts/rest.js +0 -103
  64. package/runtime/scripts/amalgm-mcp/artifacts/store.js +0 -157
  65. package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +0 -439
  66. package/runtime/scripts/amalgm-mcp/artifacts/tools.js +0 -176
  67. package/runtime/scripts/amalgm-mcp/events/executor.js +0 -258
  68. package/runtime/scripts/amalgm-mcp/events/rest.js +0 -214
  69. package/runtime/scripts/amalgm-mcp/events/tools.js +0 -323
  70. package/runtime/scripts/amalgm-mcp/tasks/rest.js +0 -110
  71. package/runtime/scripts/amalgm-mcp/tasks/tools.js +0 -416
@@ -0,0 +1,348 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+
9
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-automations-test-'));
10
+ process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
11
+
12
+ const { closeLocalDb } = require('../state/db');
13
+ const {
14
+ claimDueScheduledTriggers,
15
+ createAutomation,
16
+ listAutomationRuns,
17
+ listTriggers,
18
+ listWorkflowCellRuns,
19
+ updateAutomationByTriggerId,
20
+ } = require('../automations/store');
21
+ const { executeAutomation } = require('../automations/runner');
22
+ const { validateWorkflowText } = require('../automations/validator');
23
+ const { compileWorkflowText } = require('../workflows/compiler');
24
+ const { buildSnapshot } = require('../state/snapshot');
25
+ const automationTools = require('../automations/tools');
26
+ const { upsertTool } = require('../toolbox/store');
27
+
28
+ test.after(() => {
29
+ closeLocalDb();
30
+ fs.rmSync(tempRoot, { recursive: true, force: true });
31
+ });
32
+
33
+ test('automation stores trigger plus workflow in local live SQLite resources', () => {
34
+ const automation = createAutomation({
35
+ id: 'automation-webhook',
36
+ name: 'Webhook automation',
37
+ triggers: [{ id: 'trigger-webhook', kind: 'event', source: 'github', event: 'push' }],
38
+ workflowText: `export default workflow({
39
+ trigger: event("github.push"),
40
+ cells: [code("ok", async ({ payload }) => ({ ref: payload.ref }))]
41
+ })`,
42
+ });
43
+
44
+ assert.equal(automation.triggers.length, 1);
45
+ assert.equal(automation.workflows.length, 1);
46
+ assert.equal(automation.triggers[0].kind, 'event');
47
+
48
+ const snapshot = buildSnapshot('automations,triggers,workflows');
49
+ assert.equal(snapshot.resources.automations.some((item) => item.id === 'automation-webhook'), true);
50
+ assert.equal(snapshot.resources.triggers.some((item) => item.id === 'trigger-webhook'), true);
51
+ assert.equal(snapshot.resources.workflows.some((item) => item.id === automation.workflowId), true);
52
+ });
53
+
54
+ test('trigger update changes trigger fields without renaming parent automation', () => {
55
+ const automation = createAutomation({
56
+ id: 'automation-trigger-edit',
57
+ name: 'Parent automation',
58
+ triggers: [{ id: 'trigger-edit-target', kind: 'event', event: 'push' }],
59
+ workflowText: `export default workflow({
60
+ trigger: event("github.push"),
61
+ cells: [code("ok", async () => "done")]
62
+ })`,
63
+ });
64
+
65
+ const updated = updateAutomationByTriggerId('trigger-edit-target', {
66
+ name: 'Edited trigger',
67
+ description: 'Only the trigger should change',
68
+ event: 'release',
69
+ sourceUrl: 'https://example.test/webhook',
70
+ });
71
+
72
+ assert.equal(updated.id, automation.id);
73
+ assert.equal(updated.name, 'Parent automation');
74
+ assert.equal(updated.triggers[0].name, 'Edited trigger');
75
+ assert.equal(updated.triggers[0].description, 'Only the trigger should change');
76
+ assert.equal(updated.triggers[0].event, 'release');
77
+ assert.equal(updated.triggers[0].sourceUrl, 'https://example.test/webhook');
78
+ });
79
+
80
+ test('automation runner records unified run and cell receipts', async () => {
81
+ const automation = buildSnapshot('automations').resources.automations
82
+ .find((item) => item.id === 'automation-webhook');
83
+ const run = await executeAutomation(automation, automation.triggers[0], {
84
+ source: 'github',
85
+ event: 'push',
86
+ payload: { ref: 'refs/heads/main' },
87
+ headers: {},
88
+ });
89
+
90
+ assert.equal(run.status, 'completed');
91
+ assert.equal(run.output.ok.ref, 'refs/heads/main');
92
+ assert.equal(listAutomationRuns({ automationId: automation.id }).length, 1);
93
+ assert.equal(listWorkflowCellRuns({ runId: run.id }).length, 1);
94
+ });
95
+
96
+ test('scheduled trigger claims run once and disables one-shot trigger', () => {
97
+ createAutomation({
98
+ id: 'automation-scheduled',
99
+ name: 'Scheduled automation',
100
+ triggers: [{
101
+ id: 'trigger-scheduled',
102
+ kind: 'scheduled',
103
+ schedule: { kind: 'once', at: '2026-05-18T16:00:00.000Z' },
104
+ }],
105
+ workflowText: `export default workflow({
106
+ trigger: event("scheduled.run"),
107
+ cells: [code("ok", async () => "done")]
108
+ })`,
109
+ });
110
+
111
+ const claims = claimDueScheduledTriggers(new Date('2026-05-18T16:00:01.000Z'));
112
+ assert.equal(claims.length, 1);
113
+ const trigger = listTriggers().find((item) => item.id === 'trigger-scheduled');
114
+ assert.equal(trigger.enabled, false);
115
+ assert.equal(trigger.nextRunAt, null);
116
+ });
117
+
118
+ test('workflow ctx exposes previous cell outputs through cells aliases', async () => {
119
+ const automation = createAutomation({
120
+ id: 'automation-cells-context',
121
+ name: 'Cells context automation',
122
+ triggers: [{ id: 'trigger-cells-context', kind: 'event', source: 'test', event: 'cells' }],
123
+ workflowText: `export default workflow({
124
+ trigger: event("test.cells"),
125
+ cells: [
126
+ code("greet", async () => ({ message: "hello" })),
127
+ code("read", async ({ cells, outputs, greet }) => ({
128
+ viaOutput: cells.greet.output.message,
129
+ viaAlias: cells.greet.message,
130
+ viaOutputs: outputs.greet.message,
131
+ viaTopLevel: greet.message
132
+ }))
133
+ ]
134
+ })`,
135
+ });
136
+
137
+ const run = await executeAutomation(automation, automation.triggers[0], {
138
+ source: 'test',
139
+ event: 'cells',
140
+ payload: {},
141
+ headers: {},
142
+ });
143
+
144
+ assert.equal(run.status, 'completed');
145
+ assert.deepEqual(JSON.parse(JSON.stringify(run.output.read)), {
146
+ viaOutput: 'hello',
147
+ viaAlias: 'hello',
148
+ viaOutputs: 'hello',
149
+ viaTopLevel: 'hello',
150
+ });
151
+ });
152
+
153
+ test('workflow compiler accepts compact tool and agent helpers', () => {
154
+ const ir = compileWorkflowText(`export default workflow({
155
+ trigger: event("github.push"),
156
+ cells: [
157
+ code("greet", async () => ({ message: "hello" })),
158
+ tool("notify", "amalgm.notify_user", ({ cells }) => ({ message: cells.greet.output.message })),
159
+ agent("ask_codex", "Codex", { prompt: "Review this", run_in_background: true })
160
+ ]
161
+ })`);
162
+
163
+ assert.equal(ir.cells[1].toolId, 'amalgm');
164
+ assert.equal(ir.cells[1].actionName, 'notify_user');
165
+ assert.equal(ir.cells[2].toolId, 'amalgm');
166
+ assert.equal(ir.cells[2].actionName, 'talk_to_agent');
167
+ assert.equal(ir.cells[2].args.agent, 'Codex');
168
+ });
169
+
170
+ test('workflow compiler splits dotted toolbox action refs on the last dot', () => {
171
+ const ir = compileWorkflowText(`export default workflow({
172
+ trigger: event("test.dotted"),
173
+ cells: [
174
+ tool("quote", "codex.yahoo_stock_price.run", { symbol: "AAPL" })
175
+ ]
176
+ })`);
177
+
178
+ assert.equal(ir.cells[0].toolId, 'codex.yahoo_stock_price');
179
+ assert.equal(ir.cells[0].actionName, 'run');
180
+ });
181
+
182
+ test('automation validator catches dry-run cell reference failures without persistence', async () => {
183
+ const valid = await validateWorkflowText(`export default workflow({
184
+ trigger: event("test.validate"),
185
+ cells: [
186
+ code("greet", async () => ({ message: "hello" })),
187
+ tool("notify", "amalgm.notify_user", ({ cells }) => ({ message: cells.greet.output.message }))
188
+ ]
189
+ })`);
190
+
191
+ assert.equal(valid.ok, true);
192
+ assert.equal(valid.dryRun.cells[1].output.args.message, 'hello');
193
+
194
+ const invalid = await validateWorkflowText(`export default workflow({
195
+ trigger: event("test.validate"),
196
+ cells: [
197
+ code("greet", async () => ({ message: "hello" })),
198
+ tool("notify", "amalgm.notify_user", ({ cells }) => ({ message: cells.missing.output.message }))
199
+ ]
200
+ })`);
201
+
202
+ assert.equal(invalid.ok, false);
203
+ assert.equal(invalid.errors[0].cell, 'notify');
204
+ assert.equal(invalid.errors[0].phase, 'static');
205
+ assert.match(invalid.errors[0].message, /unknown cell "missing"/);
206
+ });
207
+
208
+ test('automation validator catches unknown tool actions before dry run', async () => {
209
+ const invalid = await validateWorkflowText(`export default workflow({
210
+ trigger: event("test.validate"),
211
+ cells: [
212
+ tool("quote", "codex.yahoo_stock_price.run", { symbol: "AAPL" })
213
+ ]
214
+ })`);
215
+
216
+ assert.equal(invalid.ok, false);
217
+ assert.equal(invalid.errors[0].phase, 'static');
218
+ assert.equal(invalid.errors[0].cell, 'quote');
219
+ assert.match(invalid.errors[0].message, /Tool action "codex\.yahoo_stock_price\.run" was not found/);
220
+ });
221
+
222
+ test('dotted toolbox action refs validate and run after the action is registered', async () => {
223
+ upsertTool({
224
+ id: 'codex.yahoo_stock_price',
225
+ name: 'Yahoo Stock Price',
226
+ type: 'cli',
227
+ source: {
228
+ command: process.execPath,
229
+ args: ['-e', 'let s=""; process.stdin.on("data", d => s += d); process.stdin.on("end", () => { const input = JSON.parse(s); console.log(JSON.stringify({ symbol: input.symbol, price: 123.45 })); });'],
230
+ inputMode: 'json-stdin',
231
+ outputMode: 'json',
232
+ },
233
+ });
234
+
235
+ const workflowText = `export default workflow({
236
+ trigger: event("test.quote"),
237
+ cells: [
238
+ tool("quote", "codex.yahoo_stock_price.run", { symbol: "AAPL" })
239
+ ]
240
+ })`;
241
+ const valid = await validateWorkflowText(workflowText);
242
+ assert.equal(valid.ok, true);
243
+
244
+ const automation = createAutomation({
245
+ id: 'automation-dotted-toolbox',
246
+ name: 'Dotted toolbox automation',
247
+ triggers: [{ id: 'trigger-dotted-toolbox', kind: 'event', source: 'test', event: 'quote' }],
248
+ workflowText,
249
+ });
250
+ const run = await executeAutomation(automation, automation.triggers[0], {
251
+ source: 'test',
252
+ event: 'quote',
253
+ payload: {},
254
+ headers: {},
255
+ });
256
+
257
+ assert.equal(run.status, 'completed');
258
+ assert.equal(run.output.quote.symbol, 'AAPL');
259
+ assert.equal(run.output.quote.price, 123.45);
260
+ });
261
+
262
+ test('automation validator catches out-of-order references before dry run', async () => {
263
+ const invalid = await validateWorkflowText(`export default workflow({
264
+ trigger: event("test.validate"),
265
+ cells: [
266
+ code("read", async ({ cells }) => cells.greet.output.message),
267
+ code("greet", async () => ({ message: "hello" }))
268
+ ]
269
+ })`);
270
+
271
+ assert.equal(invalid.ok, false);
272
+ assert.equal(invalid.errors[0].phase, 'static');
273
+ assert.equal(invalid.errors[0].cell, 'read');
274
+ assert.match(invalid.errors[0].message, /before it has run/);
275
+ });
276
+
277
+ test('automation create rejects static cell reference errors', () => {
278
+ assert.throws(() => createAutomation({
279
+ id: 'automation-invalid-reference',
280
+ name: 'Invalid reference automation',
281
+ triggers: [{ id: 'trigger-invalid-reference', kind: 'event', source: 'test', event: 'invalid' }],
282
+ workflowText: `export default workflow({
283
+ trigger: event("test.invalid"),
284
+ cells: [
285
+ code("read", async ({ cells }) => cells.missing.output.message)
286
+ ]
287
+ })`,
288
+ }), /unknown cell "missing"/);
289
+ });
290
+
291
+ test('automation validator uses mock_outputs for dry-run tool consumers', async () => {
292
+ const valid = await validateWorkflowText(`export default workflow({
293
+ trigger: event("test.validate"),
294
+ cells: [
295
+ tool("ping", "http.fetch", { url: "https://example.com" }),
296
+ code("read", async ({ cells }) => cells.ping.output.body.url)
297
+ ]
298
+ })`, {
299
+ mock_outputs: {
300
+ ping: { status: 200, body: { url: "https://example.com" } },
301
+ },
302
+ });
303
+
304
+ assert.equal(valid.ok, true);
305
+ assert.equal(valid.dryRun.output.read, 'https://example.com');
306
+ });
307
+
308
+ test('automations_create supports concise verbose false responses', async () => {
309
+ const createTool = automationTools.find((tool) => tool.name === 'automations_create');
310
+ const result = await createTool.handler({
311
+ id: 'automation-compact-response',
312
+ name: 'Compact response automation',
313
+ verbose: false,
314
+ triggers: [{ id: 'trigger-compact-response', kind: 'event', source: 'test', event: 'compact' }],
315
+ workflowText: `export default workflow({
316
+ trigger: event("test.compact"),
317
+ cells: [code("ok", async () => ({ ok: true }))]
318
+ })`,
319
+ });
320
+ const text = result.content[0].text;
321
+ const payload = JSON.parse(text.slice(text.indexOf('{')));
322
+
323
+ assert.equal(payload.id, 'automation-compact-response');
324
+ assert.equal(payload.triggers[0].id, 'trigger-compact-response');
325
+ assert.equal(payload.triggers[0].ref, 'test.compact');
326
+ assert.equal(payload.workflow.id, payload.workflowIds[0]);
327
+ assert.equal(payload.workflow.workflowText, undefined);
328
+ });
329
+
330
+ test('automations tools list actions and compact run_now output', async () => {
331
+ const listActionsTool = automationTools.find((tool) => tool.name === 'automations_list_actions');
332
+ const runNowTool = automationTools.find((tool) => tool.name === 'automations_run_now');
333
+
334
+ const listResult = await listActionsTool.handler({ query: 'yahoo' });
335
+ const listPayload = JSON.parse(listResult.content[0].text);
336
+ assert.equal(listPayload.actions.some((action) => action.ref === 'codex.yahoo_stock_price.run'), true);
337
+
338
+ const runResult = await runNowTool.handler({
339
+ automation_id: 'automation-dotted-toolbox',
340
+ verbose: false,
341
+ });
342
+ const text = runResult.content[0].text;
343
+ const payload = JSON.parse(text.slice(text.indexOf('{')));
344
+ assert.equal(payload.status, 'completed');
345
+ assert.deepEqual(payload.outputKeys, ['quote']);
346
+ assert.equal(payload.output, undefined);
347
+ assert.equal(payload.cellSummaries[0].name, 'quote');
348
+ });
@@ -6,6 +6,7 @@ const assert = require('node:assert/strict');
6
6
  const {
7
7
  computeSignature,
8
8
  extractSignature,
9
+ matchAllBySignature,
9
10
  matchBySignature,
10
11
  } = require('../events/matcher');
11
12
 
@@ -43,3 +44,25 @@ test('event matcher rejects disabled triggers and wrong secrets', () => {
43
44
 
44
45
  assert.equal(matchBySignature([trigger(), trigger({ id: 'disabled', enabled: false })], signature, rawBody), null);
45
46
  });
47
+
48
+ test('event matcher filters by source and event when provided', () => {
49
+ const rawBody = JSON.stringify({ ok: true });
50
+ const signature = extractSignature({
51
+ 'x-webhook-signature': computeSignature('super-secret', rawBody),
52
+ });
53
+
54
+ const matches = matchAllBySignature([
55
+ trigger({ id: 'push-main', source: 'github', event: 'push' }),
56
+ trigger({ id: 'issue-opened', source: 'github', event: 'issues' }),
57
+ trigger({ id: 'wildcard', source: 'github', event: '*' }),
58
+ ], signature, rawBody, { source: 'github', event: 'push' });
59
+
60
+ assert.deepEqual(matches.map((item) => item.id), ['push-main', 'wildcard']);
61
+ assert.equal(
62
+ matchBySignature([trigger({ source: 'github', event: 'issues' })], signature, rawBody, {
63
+ source: 'github',
64
+ event: 'push',
65
+ }),
66
+ null,
67
+ );
68
+ });
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+
9
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-workflows-test-'));
10
+ process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
11
+
12
+ const { closeLocalDb } = require('../state/db');
13
+ const { loadEventTriggers, saveEventTriggers } = require('../events/store');
14
+ const { listEventRuns } = require('../events/store');
15
+ const { createWorkflow } = require('../workflows/store');
16
+ const { executeEventWorkflow } = require('../workflows/runner');
17
+
18
+ test.after(() => {
19
+ closeLocalDb();
20
+ fs.rmSync(tempRoot, { recursive: true, force: true });
21
+ });
22
+
23
+ test('event trigger can run a linked standalone workflow', async () => {
24
+ const workflow = createWorkflow({
25
+ id: 'workflow-publish',
26
+ name: 'Publish workflow',
27
+ workflowText: `export default workflow({
28
+ trigger: event("github.push"),
29
+ cells: [
30
+ code("filter_push", async ({ payload }) => {
31
+ return { sha: payload.after }
32
+ })
33
+ ]
34
+ })`,
35
+ });
36
+
37
+ saveEventTriggers({
38
+ version: 2,
39
+ triggers: [{
40
+ id: 'trigger-github-push',
41
+ name: 'GitHub push',
42
+ source: 'github',
43
+ event: 'push',
44
+ enabled: true,
45
+ secret: 'secret',
46
+ workflowIds: [workflow.id],
47
+ createdAt: '2026-05-23T00:00:00.000Z',
48
+ }],
49
+ });
50
+
51
+ const trigger = loadEventTriggers().triggers.find((item) => item.id === 'trigger-github-push');
52
+ assert.deepEqual(trigger.workflowIds, ['workflow-publish']);
53
+
54
+ await executeEventWorkflow(trigger, {
55
+ source: 'github',
56
+ event: 'push',
57
+ payload: { after: 'abc123' },
58
+ headers: {},
59
+ timestamp: '2026-05-23T00:00:01.000Z',
60
+ });
61
+
62
+ const [run] = listEventRuns({ triggerId: trigger.id });
63
+ assert.equal(run.status, 'completed');
64
+ assert.equal(run.workflowId, 'workflow-publish');
65
+ assert.equal(run.workflowName, 'Publish workflow');
66
+ assert.equal(run.output.filter_push.sha, 'abc123');
67
+ });
@@ -12,8 +12,20 @@ const {
12
12
  upsertTool,
13
13
  upsertToolAction,
14
14
  } = require('./store');
15
+ const { toolboxMcpToolName } = require('./runner');
15
16
 
16
- function compactTool(tool, actionCount) {
17
+ function compactAction(action) {
18
+ return {
19
+ id: action.id,
20
+ name: action.name,
21
+ ref: `${action.toolId}.${action.name}`,
22
+ status: action.status,
23
+ description: action.description,
24
+ mcpToolName: toolboxMcpToolName(action),
25
+ };
26
+ }
27
+
28
+ function compactTool(tool, actions) {
17
29
  return {
18
30
  id: tool.id,
19
31
  name: tool.name,
@@ -22,7 +34,8 @@ function compactTool(tool, actionCount) {
22
34
  origin: tool.origin,
23
35
  status: tool.status,
24
36
  discovery: tool.discovery,
25
- actionCount,
37
+ actions: actions.map(compactAction),
38
+ actionCount: actions.length,
26
39
  source: tool.source,
27
40
  guide: tool.guide,
28
41
  display: tool.display,
@@ -43,7 +56,7 @@ function listPayload(args = {}) {
43
56
  .filter((tool) => !args.type || tool.type === args.type)
44
57
  .filter((tool) => !args.owner || tool.owner === args.owner)
45
58
  .filter((tool) => !args.origin || tool.origin === args.origin)
46
- .map((tool) => compactTool(tool, actionsByTool.get(tool.id)?.length || 0));
59
+ .map((tool) => compactTool(tool, actionsByTool.get(tool.id) || []));
47
60
 
48
61
  const payload = {
49
62
  version: toolbox.version,