@amalgm/automations 0.2.6 → 0.3.0
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/AXIOMS.md +47 -8
- package/PURPOSE.md +45 -11
- package/README.md +116 -12
- package/dist/src/cli/arguments.d.ts +20 -0
- package/dist/src/cli/arguments.js +49 -0
- package/dist/src/cli/input.d.ts +6 -0
- package/dist/src/cli/input.js +48 -0
- package/dist/src/cli/main.d.ts +7 -0
- package/dist/src/cli/main.js +39 -0
- package/dist/src/cli/run.d.ts +13 -0
- package/dist/src/cli/run.js +51 -0
- package/dist/src/cli/shell-runtime.d.ts +9 -0
- package/dist/src/cli/shell-runtime.js +102 -0
- package/dist/src/cli-main.js +2 -20
- package/dist/src/cli.d.ts +3 -10
- package/dist/src/cli.js +3 -186
- package/dist/src/command-result.d.ts +8 -0
- package/dist/src/command-result.js +13 -0
- package/dist/src/command-surface.d.ts +25 -0
- package/dist/src/command-surface.js +125 -0
- package/dist/src/contract.d.ts +1 -1
- package/dist/src/execution-contract.d.ts +28 -0
- package/dist/src/execution-contract.js +1 -0
- package/dist/src/execution-errors.d.ts +6 -0
- package/dist/src/execution-errors.js +48 -0
- package/dist/src/executor.d.ts +3 -16
- package/dist/src/executor.js +31 -36
- package/dist/src/index.d.ts +7 -3
- package/dist/src/index.js +6 -2
- package/dist/src/input-template.d.ts +2 -2
- package/dist/src/input-template.js +4 -4
- package/dist/src/mcp-main.js +0 -0
- package/dist/src/mcp.js +15 -79
- package/dist/src/node-process-host.d.ts +13 -0
- package/dist/src/node-process-host.js +116 -0
- package/dist/src/node-process-runner.d.ts +23 -0
- package/dist/src/node-process-runner.js +140 -0
- package/dist/src/plan.js +125 -9
- package/dist/src/run-contract.d.ts +41 -1
- package/dist/src/run-journal.d.ts +2 -2
- package/dist/src/schema.d.ts +4 -4
- package/package.json +3 -3
- package/skills/automations/SKILL.md +69 -6
package/dist/src/cli-main.js
CHANGED
|
@@ -1,21 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
const baseUrl = process.env.AMALGM_AUTOMATIONS_API_URL;
|
|
5
|
-
const authorization = process.env.AMALGM_AUTOMATIONS_AUTHORIZATION;
|
|
6
|
-
const argv = process.argv.slice(2);
|
|
7
|
-
const help = argv.length === 0 || argv[0] === 'help' || argv[0] === '--help' || argv[0] === '-h';
|
|
8
|
-
if (help) {
|
|
9
|
-
process.stdout.write(automationCliHelp);
|
|
10
|
-
}
|
|
11
|
-
else if (!baseUrl || !authorization) {
|
|
12
|
-
process.stderr.write('AMALGM_AUTOMATIONS_API_URL and AMALGM_AUTOMATIONS_AUTHORIZATION are required.\n');
|
|
13
|
-
process.exitCode = 1;
|
|
14
|
-
}
|
|
15
|
-
else {
|
|
16
|
-
const code = await runAutomationCli(argv, createAutomationClient({
|
|
17
|
-
baseUrl,
|
|
18
|
-
authorization: () => authorization,
|
|
19
|
-
}));
|
|
20
|
-
process.exitCode = code;
|
|
21
|
-
}
|
|
2
|
+
import { runAutomationCliMain } from './cli/main.js';
|
|
3
|
+
process.exitCode = await runAutomationCliMain();
|
package/dist/src/cli.d.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
export declare const automationCliHelp = "Usage: amalgm-automations <resource> <action>\n\nautomations create|list|get|update|delete\ntriggers list | schedule <create|list|get|update|delete> | webhook <create|list|get|update|delete>\nworkflow create|get|update|delete\nruns run-now|list|get\n\nList filters:\n automations list [--target-id ID] [--enabled true|false] [--limit N] [--offset N]\n triggers schedule|webhook list AUTOMATION_ID [--limit N] [--offset N]\n runs list AUTOMATION_ID [--status STATUS] [--limit N] [--offset N]\n";
|
|
6
|
-
export declare function runAutomationCli(argv: string[], sdk: AutomationCrud, output?: {
|
|
7
|
-
stdout?: Output;
|
|
8
|
-
stderr?: Output;
|
|
9
|
-
}): Promise<number>;
|
|
10
|
-
export {};
|
|
1
|
+
export { automationCliHelp, runAutomationCli, type AutomationCliIo, } from './cli/run.js';
|
|
2
|
+
export { createShellAutomationCommands, shellAutomationEndpoint, type ShellAutomationCommandOptions, } from './cli/shell-runtime.js';
|
|
3
|
+
export { runAutomationCliMain, type AutomationCliMainOptions } from './cli/main.js';
|
package/dist/src/cli.js
CHANGED
|
@@ -1,186 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
automations create|list|get|update|delete
|
|
5
|
-
triggers list | schedule <create|list|get|update|delete> | webhook <create|list|get|update|delete>
|
|
6
|
-
workflow create|get|update|delete
|
|
7
|
-
runs run-now|list|get
|
|
8
|
-
|
|
9
|
-
List filters:
|
|
10
|
-
automations list [--target-id ID] [--enabled true|false] [--limit N] [--offset N]
|
|
11
|
-
triggers schedule|webhook list AUTOMATION_ID [--limit N] [--offset N]
|
|
12
|
-
runs list AUTOMATION_ID [--status STATUS] [--limit N] [--offset N]
|
|
13
|
-
`;
|
|
14
|
-
export async function runAutomationCli(argv, sdk, output = {}) {
|
|
15
|
-
const stdout = output.stdout || process.stdout;
|
|
16
|
-
const stderr = output.stderr || process.stderr;
|
|
17
|
-
try {
|
|
18
|
-
const [resource, action, ...rest] = argv;
|
|
19
|
-
if (!resource || resource === 'help' || resource === '--help' || resource === '-h') {
|
|
20
|
-
stdout.write(automationCliHelp);
|
|
21
|
-
return 0;
|
|
22
|
-
}
|
|
23
|
-
const result = await command(resource, action, rest, sdk);
|
|
24
|
-
stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
25
|
-
return 0;
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
29
|
-
return 1;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
async function command(resource, action, args, sdk) {
|
|
33
|
-
if (resource === 'automations')
|
|
34
|
-
return automationCommand(action, args, sdk);
|
|
35
|
-
if (resource === 'triggers')
|
|
36
|
-
return triggerCommand(action, args, sdk);
|
|
37
|
-
if (resource === 'workflow')
|
|
38
|
-
return workflowCommand(action, args, sdk);
|
|
39
|
-
if (resource === 'runs')
|
|
40
|
-
return runsCommand(action, args, sdk);
|
|
41
|
-
throw new Error(`Unknown resource: ${resource}`);
|
|
42
|
-
}
|
|
43
|
-
async function automationCommand(action, args, sdk) {
|
|
44
|
-
if (action === 'create')
|
|
45
|
-
return sdk.automations.create(await jsonFile(required(args, 0, 'automation JSON file')));
|
|
46
|
-
if (action === 'list')
|
|
47
|
-
return sdk.automations.list(automationListOptions(args));
|
|
48
|
-
if (action === 'get')
|
|
49
|
-
return sdk.automations.get(required(args, 0, 'automation id'));
|
|
50
|
-
if (action === 'update')
|
|
51
|
-
return sdk.automations.update(required(args, 0, 'automation id'), await jsonFile(required(args, 1, 'automation patch JSON file')));
|
|
52
|
-
if (action === 'delete') {
|
|
53
|
-
const automationId = required(args, 0, 'automation id');
|
|
54
|
-
await sdk.automations.delete(automationId);
|
|
55
|
-
return { deleted: automationId };
|
|
56
|
-
}
|
|
57
|
-
throw new Error('Usage: automations <create|list|get|update|delete>');
|
|
58
|
-
}
|
|
59
|
-
async function triggerCommand(action, args, sdk) {
|
|
60
|
-
if (action === 'list')
|
|
61
|
-
return sdk.triggers.list(required(args, 0, 'automation id'));
|
|
62
|
-
const [operation, automationId, ...rest] = args;
|
|
63
|
-
const automation = requiredValue(automationId, 'automation id');
|
|
64
|
-
if (action === 'schedule') {
|
|
65
|
-
if (operation === 'create')
|
|
66
|
-
return sdk.triggers.schedule.create(automation, await jsonFile(required(rest, 0, 'schedule JSON file')));
|
|
67
|
-
if (operation === 'list')
|
|
68
|
-
return sdk.triggers.schedule.list(automation, pageOptions(rest));
|
|
69
|
-
if (operation === 'get')
|
|
70
|
-
return sdk.triggers.schedule.get(automation, required(rest, 0, 'schedule trigger id'));
|
|
71
|
-
if (operation === 'update')
|
|
72
|
-
return sdk.triggers.schedule.update(automation, required(rest, 0, 'schedule trigger id'), await jsonFile(required(rest, 1, 'schedule patch JSON file')));
|
|
73
|
-
if (operation === 'delete') {
|
|
74
|
-
const id = required(rest, 0, 'schedule trigger id');
|
|
75
|
-
await sdk.triggers.schedule.delete(automation, id);
|
|
76
|
-
return { deleted: id };
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if (action === 'webhook') {
|
|
80
|
-
if (operation === 'create')
|
|
81
|
-
return sdk.triggers.webhook.create(automation, await jsonFile(required(rest, 0, 'webhook JSON file')));
|
|
82
|
-
if (operation === 'list')
|
|
83
|
-
return sdk.triggers.webhook.list(automation, pageOptions(rest));
|
|
84
|
-
if (operation === 'get')
|
|
85
|
-
return sdk.triggers.webhook.get(automation, required(rest, 0, 'webhook trigger id'));
|
|
86
|
-
if (operation === 'update')
|
|
87
|
-
return sdk.triggers.webhook.update(automation, required(rest, 0, 'webhook trigger id'), await jsonFile(required(rest, 1, 'webhook patch JSON file')));
|
|
88
|
-
if (operation === 'delete') {
|
|
89
|
-
const id = required(rest, 0, 'webhook trigger id');
|
|
90
|
-
await sdk.triggers.webhook.delete(automation, id);
|
|
91
|
-
return { deleted: id };
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
throw new Error('Usage: triggers <list|schedule|webhook>');
|
|
95
|
-
}
|
|
96
|
-
async function workflowCommand(action, args, sdk) {
|
|
97
|
-
const automationId = required(args, 0, 'automation id');
|
|
98
|
-
if (action === 'create')
|
|
99
|
-
return sdk.workflow.create(automationId, await jsonFile(required(args, 1, 'workflow JSON file')));
|
|
100
|
-
if (action === 'get')
|
|
101
|
-
return sdk.workflow.get(automationId);
|
|
102
|
-
if (action === 'update')
|
|
103
|
-
return sdk.workflow.update(automationId, await jsonFile(required(args, 1, 'workflow patch JSON file')));
|
|
104
|
-
if (action === 'delete') {
|
|
105
|
-
await sdk.workflow.delete(automationId);
|
|
106
|
-
return { deleted: automationId };
|
|
107
|
-
}
|
|
108
|
-
throw new Error('Usage: workflow <create|get|update|delete>');
|
|
109
|
-
}
|
|
110
|
-
async function runsCommand(action, args, sdk) {
|
|
111
|
-
const automationId = required(args, 0, 'automation id');
|
|
112
|
-
if (action === 'run-now') {
|
|
113
|
-
return sdk.runs.runNow(automationId, args[1] ? await jsonFile(args[1]) : {});
|
|
114
|
-
}
|
|
115
|
-
if (action === 'list')
|
|
116
|
-
return sdk.runs.list(automationId, runListOptions(args.slice(1)));
|
|
117
|
-
if (action === 'get')
|
|
118
|
-
return sdk.runs.get(automationId, required(args, 1, 'run id'));
|
|
119
|
-
throw new Error('Usage: runs <run-now|list|get>');
|
|
120
|
-
}
|
|
121
|
-
function automationListOptions(args) {
|
|
122
|
-
const options = readOptions(args, new Set(['--target-id', '--enabled', '--limit', '--offset']));
|
|
123
|
-
return {
|
|
124
|
-
...pageOptionsFrom(options),
|
|
125
|
-
...(options['--target-id'] ? { targetId: options['--target-id'] } : {}),
|
|
126
|
-
...(options['--enabled'] === undefined ? {} : { enabled: booleanOption(options['--enabled'], '--enabled') }),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
function runListOptions(args) {
|
|
130
|
-
const options = readOptions(args, new Set(['--status', '--limit', '--offset']));
|
|
131
|
-
return {
|
|
132
|
-
...pageOptionsFrom(options),
|
|
133
|
-
...(options['--status'] ? { status: options['--status'] } : {}),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
function pageOptions(args) {
|
|
137
|
-
return pageOptionsFrom(readOptions(args, new Set(['--limit', '--offset'])));
|
|
138
|
-
}
|
|
139
|
-
function pageOptionsFrom(options) {
|
|
140
|
-
return {
|
|
141
|
-
...(options['--limit'] === undefined ? {} : { limit: numberOption(options['--limit'], '--limit') }),
|
|
142
|
-
...(options['--offset'] === undefined ? {} : { offset: numberOption(options['--offset'], '--offset') }),
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
function readOptions(args, allowed) {
|
|
146
|
-
const result = {};
|
|
147
|
-
for (let index = 0; index < args.length; index += 2) {
|
|
148
|
-
const flag = args[index];
|
|
149
|
-
const value = args[index + 1];
|
|
150
|
-
if (!flag || !allowed.has(flag) || value === undefined)
|
|
151
|
-
throw new Error(`Unknown or incomplete list option: ${flag || ''}`);
|
|
152
|
-
if (result[flag] !== undefined)
|
|
153
|
-
throw new Error(`List option may be provided once: ${flag}`);
|
|
154
|
-
result[flag] = value;
|
|
155
|
-
}
|
|
156
|
-
return result;
|
|
157
|
-
}
|
|
158
|
-
function numberOption(value, name) {
|
|
159
|
-
const number = Number(value);
|
|
160
|
-
if (!Number.isInteger(number))
|
|
161
|
-
throw new Error(`${name} must be an integer`);
|
|
162
|
-
return number;
|
|
163
|
-
}
|
|
164
|
-
function booleanOption(value, name) {
|
|
165
|
-
if (value === 'true')
|
|
166
|
-
return true;
|
|
167
|
-
if (value === 'false')
|
|
168
|
-
return false;
|
|
169
|
-
throw new Error(`${name} must be true or false`);
|
|
170
|
-
}
|
|
171
|
-
async function jsonFile(filename) {
|
|
172
|
-
try {
|
|
173
|
-
return JSON.parse(await fs.readFile(filename, 'utf8'));
|
|
174
|
-
}
|
|
175
|
-
catch (error) {
|
|
176
|
-
throw new Error(`Cannot read JSON file ${filename}: ${error instanceof Error ? error.message : String(error)}`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
function required(values, index, label) {
|
|
180
|
-
return requiredValue(values[index], label);
|
|
181
|
-
}
|
|
182
|
-
function requiredValue(value, label) {
|
|
183
|
-
if (!value)
|
|
184
|
-
throw new Error(`${label} is required`);
|
|
185
|
-
return value;
|
|
186
|
-
}
|
|
1
|
+
export { automationCliHelp, runAutomationCli, } from './cli/run.js';
|
|
2
|
+
export { createShellAutomationCommands, shellAutomationEndpoint, } from './cli/shell-runtime.js';
|
|
3
|
+
export { runAutomationCliMain } from './cli/main.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AutomationError } from './errors.js';
|
|
2
|
+
export function automationFailure(error) {
|
|
3
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4
|
+
const code = error instanceof AutomationError ? error.code : 'internal';
|
|
5
|
+
const status = error instanceof AutomationError ? error.status : undefined;
|
|
6
|
+
return {
|
|
7
|
+
error: {
|
|
8
|
+
code,
|
|
9
|
+
message,
|
|
10
|
+
...(status === undefined ? {} : { status }),
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from 'zod/v3';
|
|
2
|
+
import type { AutomationCrud } from './contract.js';
|
|
3
|
+
import { type AutomationToolSurface } from './tool-surface.js';
|
|
4
|
+
export type AutomationCommandName = 'create' | 'list' | 'get' | 'update' | 'delete' | 'run-now';
|
|
5
|
+
export interface AutomationCommandAnnotations {
|
|
6
|
+
readOnlyHint: boolean;
|
|
7
|
+
destructiveHint: boolean;
|
|
8
|
+
idempotentHint: boolean;
|
|
9
|
+
openWorldHint: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface AutomationCommandDefinition {
|
|
12
|
+
name: AutomationCommandName;
|
|
13
|
+
mcpName: `amalgm_automations_${string}`;
|
|
14
|
+
title: string;
|
|
15
|
+
description: string;
|
|
16
|
+
inputShape: z.ZodRawShape;
|
|
17
|
+
annotations: AutomationCommandAnnotations;
|
|
18
|
+
invoke(tools: AutomationToolSurface, input: unknown): Promise<unknown>;
|
|
19
|
+
}
|
|
20
|
+
export interface AutomationCommands {
|
|
21
|
+
execute(name: AutomationCommandName, input?: unknown): Promise<unknown>;
|
|
22
|
+
}
|
|
23
|
+
export declare const automationCommandDefinitions: readonly AutomationCommandDefinition[];
|
|
24
|
+
export declare function createAutomationCommands(sdk: AutomationCrud): AutomationCommands;
|
|
25
|
+
export declare function automationCommandDefinition(name: string): AutomationCommandDefinition;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { z } from 'zod/v3';
|
|
2
|
+
import { ValidationError } from './errors.js';
|
|
3
|
+
import { createAutomationSchema, createScheduleTriggerSchema, createWebhookTriggerSchema, createWorkflowSchema, identifierSchema, listAutomationsSchema, listRunsSchema, parse, runAutomationNowSchema, updateAutomationSchema, updateScheduleTriggerSchema, updateWebhookTriggerSchema, updateWorkflowSchema, } from './schema.js';
|
|
4
|
+
import { createAutomationToolSurface, } from './tool-surface.js';
|
|
5
|
+
const scheduleChangesSchema = z.object({
|
|
6
|
+
create: z.array(createScheduleTriggerSchema).max(100).optional(),
|
|
7
|
+
update: z.array(z.object({ id: identifierSchema, patch: updateScheduleTriggerSchema }).strict()).max(100).optional(),
|
|
8
|
+
delete: z.array(identifierSchema).max(100).optional(),
|
|
9
|
+
}).strict();
|
|
10
|
+
const webhookChangesSchema = z.object({
|
|
11
|
+
create: z.array(createWebhookTriggerSchema).max(100).optional(),
|
|
12
|
+
update: z.array(z.object({ id: identifierSchema, patch: updateWebhookTriggerSchema }).strict()).max(100).optional(),
|
|
13
|
+
delete: z.array(identifierSchema).max(100).optional(),
|
|
14
|
+
}).strict();
|
|
15
|
+
const workflowChangeSchema = z.union([
|
|
16
|
+
z.object({ create: createWorkflowSchema }).strict(),
|
|
17
|
+
z.object({ update: updateWorkflowSchema }).strict(),
|
|
18
|
+
z.object({ delete: z.literal(true) }).strict(),
|
|
19
|
+
]);
|
|
20
|
+
const definitions = [
|
|
21
|
+
define({
|
|
22
|
+
name: 'create',
|
|
23
|
+
title: 'Create automation',
|
|
24
|
+
description: 'Create one complete automation definition, including its schedules, webhooks, and optional workflow. Compiled version 1 remains action-only ({id, actionId, input}); use version 2 to mix Amalgm action steps with direct argv command steps ({id, kind:"command", command, args?, cwd?, env?, stdin?, timeoutMs?, maxOutputBytes?}) and explicit code steps ({id, kind:"script", runtime:"shell"|"node"|"python", source, args?, cwd?, env?, stdin?, timeoutMs?, maxOutputBytes?}). The service stages configuration disabled and enables it only after setup succeeds. Omit targetId in a machine-bound session; never guess a target id.',
|
|
25
|
+
schema: createAutomationSchema.extend({
|
|
26
|
+
schedules: z.array(createScheduleTriggerSchema).max(100).optional(),
|
|
27
|
+
webhooks: z.array(createWebhookTriggerSchema).max(100).optional(),
|
|
28
|
+
workflow: createWorkflowSchema.optional(),
|
|
29
|
+
}).strict(),
|
|
30
|
+
annotations: write(false),
|
|
31
|
+
run: (tools, input) => tools.create(input),
|
|
32
|
+
}),
|
|
33
|
+
define({
|
|
34
|
+
name: 'list',
|
|
35
|
+
title: 'List automations',
|
|
36
|
+
description: 'List the caller\'s automations with optional target, enabled-state, and pagination filters.',
|
|
37
|
+
schema: listAutomationsSchema,
|
|
38
|
+
annotations: read(),
|
|
39
|
+
run: (tools, input) => tools.list(input),
|
|
40
|
+
}),
|
|
41
|
+
define({
|
|
42
|
+
name: 'get',
|
|
43
|
+
title: 'Get automation',
|
|
44
|
+
description: 'Get one complete automation definition with its typed triggers and workflow. Recent run history is optional.',
|
|
45
|
+
schema: z.object({
|
|
46
|
+
automation_id: identifierSchema,
|
|
47
|
+
include_runs: z.boolean().optional(),
|
|
48
|
+
run_query: listRunsSchema.optional(),
|
|
49
|
+
}).strict(),
|
|
50
|
+
annotations: read(),
|
|
51
|
+
run: (tools, { automation_id, include_runs, run_query }) => (tools.get(automation_id, include_runs || run_query ? (run_query ?? { limit: 20 }) : false)),
|
|
52
|
+
}),
|
|
53
|
+
define({
|
|
54
|
+
name: 'update',
|
|
55
|
+
title: 'Update automation',
|
|
56
|
+
description: 'Update an automation and grouped trigger or workflow changes in one task-level call. Workflow plans use the same action, command, and script step forms accepted by create. Resource changes are staged while the automation is disabled; a failed change leaves a safe disabled draft.',
|
|
57
|
+
schema: z.object({
|
|
58
|
+
automation_id: identifierSchema,
|
|
59
|
+
patch: updateAutomationSchema.optional(),
|
|
60
|
+
schedules: scheduleChangesSchema.optional(),
|
|
61
|
+
webhooks: webhookChangesSchema.optional(),
|
|
62
|
+
workflow: workflowChangeSchema.optional(),
|
|
63
|
+
}).strict(),
|
|
64
|
+
annotations: write(false),
|
|
65
|
+
run: (tools, { automation_id, ...input }) => tools.update(automation_id, input),
|
|
66
|
+
}),
|
|
67
|
+
define({
|
|
68
|
+
name: 'delete',
|
|
69
|
+
title: 'Delete automation',
|
|
70
|
+
description: 'Delete an automation and its current triggers and workflow. Historical runs remain.',
|
|
71
|
+
schema: z.object({ automation_id: identifierSchema }).strict(),
|
|
72
|
+
annotations: destructive(),
|
|
73
|
+
run: (tools, { automation_id }) => tools.delete(automation_id),
|
|
74
|
+
}),
|
|
75
|
+
define({
|
|
76
|
+
name: 'run-now',
|
|
77
|
+
title: 'Run automation now',
|
|
78
|
+
description: 'Durably admit one manual run using the automation\'s current enabled configuration and compiled workflow. The selected machine executes it through the normal pending-run queue; this call never executes the workflow inline.',
|
|
79
|
+
schema: z.object({
|
|
80
|
+
automation_id: identifierSchema,
|
|
81
|
+
input: runAutomationNowSchema.shape.input,
|
|
82
|
+
idempotency_key: runAutomationNowSchema.shape.idempotencyKey,
|
|
83
|
+
}).strict(),
|
|
84
|
+
annotations: write(false),
|
|
85
|
+
run: (tools, { automation_id, input, idempotency_key }) => tools.runNow(automation_id, {
|
|
86
|
+
...(input === undefined ? {} : { input }),
|
|
87
|
+
...(idempotency_key === undefined ? {} : { idempotencyKey: idempotency_key }),
|
|
88
|
+
}),
|
|
89
|
+
}),
|
|
90
|
+
];
|
|
91
|
+
export const automationCommandDefinitions = Object.freeze(definitions);
|
|
92
|
+
export function createAutomationCommands(sdk) {
|
|
93
|
+
const tools = createAutomationToolSurface(sdk);
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
execute(name, input = {}) {
|
|
96
|
+
return automationCommandDefinition(name).invoke(tools, input);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export function automationCommandDefinition(name) {
|
|
101
|
+
const definition = automationCommandDefinitions.find((candidate) => candidate.name === name);
|
|
102
|
+
if (!definition)
|
|
103
|
+
throw new ValidationError(`Unknown Automations command: ${name}`);
|
|
104
|
+
return definition;
|
|
105
|
+
}
|
|
106
|
+
function define(input) {
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
name: input.name,
|
|
109
|
+
mcpName: `amalgm_automations_${input.name.replace('-', '_')}`,
|
|
110
|
+
title: input.title,
|
|
111
|
+
description: input.description,
|
|
112
|
+
inputShape: input.schema.shape,
|
|
113
|
+
annotations: input.annotations,
|
|
114
|
+
invoke: (tools, value) => input.run(tools, parse(input.schema, value)),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function read() {
|
|
118
|
+
return Object.freeze({ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false });
|
|
119
|
+
}
|
|
120
|
+
function write(idempotentHint) {
|
|
121
|
+
return Object.freeze({ readOnlyHint: false, destructiveHint: false, idempotentHint, openWorldHint: false });
|
|
122
|
+
}
|
|
123
|
+
function destructive() {
|
|
124
|
+
return Object.freeze({ readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false });
|
|
125
|
+
}
|
package/dist/src/contract.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export type Json = null | boolean | number | string | Json[] | {
|
|
|
2
2
|
[key: string]: Json;
|
|
3
3
|
};
|
|
4
4
|
import type { AutomationRun, ListRuns, RunAutomationNow } from './run-contract.js';
|
|
5
|
-
export type { AutomationPlan, AutomationRun, AutomationRunJournal, AutomationStepRun, AutomationStepStatus, ListRuns, RunAutomationNow, RunStatus, ToolActionStep, } from './run-contract.js';
|
|
5
|
+
export type { AutomationActionPlan, AutomationPlan, AutomationRun, AutomationRunJournal, AutomationScriptRuntime, AutomationStep, AutomationStepRun, AutomationStepStatus, AutomationTypedPlan, CodeScriptStep, DirectCommandStep, ListRuns, RunAutomationNow, RunStatus, ToolActionStep, } from './run-contract.js';
|
|
6
6
|
export type AutomationScope = 'automations:read' | 'automations:write' | 'runs:read' | 'runs:execute' | '*';
|
|
7
7
|
/** Identity resolved by Amalgm before the SDK is bound to a caller. */
|
|
8
8
|
export interface AutomationPrincipal {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CodeScriptStep, DirectCommandStep, Json } from './contract.js';
|
|
2
|
+
export interface AutomationActionPort {
|
|
3
|
+
call(input: Readonly<{
|
|
4
|
+
actionId: string;
|
|
5
|
+
payload: Json;
|
|
6
|
+
idempotencyKey: string;
|
|
7
|
+
signal: AbortSignal;
|
|
8
|
+
}>): Promise<Json>;
|
|
9
|
+
}
|
|
10
|
+
export type AutomationProcessExecution = Readonly<Omit<DirectCommandStep, 'id' | 'stdin'> & {
|
|
11
|
+
stdin?: string;
|
|
12
|
+
idempotencyKey: string;
|
|
13
|
+
signal: AbortSignal;
|
|
14
|
+
} | Omit<CodeScriptStep, 'id' | 'stdin'> & {
|
|
15
|
+
stdin?: string;
|
|
16
|
+
idempotencyKey: string;
|
|
17
|
+
signal: AbortSignal;
|
|
18
|
+
}>;
|
|
19
|
+
export interface AutomationProcessPort {
|
|
20
|
+
execute(input: AutomationProcessExecution): Promise<Json>;
|
|
21
|
+
}
|
|
22
|
+
export interface AutomationRunExecutorOptions {
|
|
23
|
+
readonly now?: () => Date;
|
|
24
|
+
readonly heartbeatMs?: number;
|
|
25
|
+
readonly maxAttempts?: number;
|
|
26
|
+
readonly retryDelayMs?: (attempt: number) => number;
|
|
27
|
+
readonly processes?: AutomationProcessPort;
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function transientFailure(error) {
|
|
2
|
+
for (let current = error, depth = 0; current && depth < 4; depth += 1) {
|
|
3
|
+
if (typeof current !== 'object')
|
|
4
|
+
break;
|
|
5
|
+
const item = current;
|
|
6
|
+
const status = Number(item.status);
|
|
7
|
+
if (status === 408 || status === 429 || status >= 500)
|
|
8
|
+
return true;
|
|
9
|
+
const code = String(item.code ?? '');
|
|
10
|
+
if (TRANSIENT_CODES.has(code))
|
|
11
|
+
return true;
|
|
12
|
+
current = item.cause;
|
|
13
|
+
}
|
|
14
|
+
const message = safeError(error).toLowerCase();
|
|
15
|
+
return message.includes('fetch failed') || message.includes('network error');
|
|
16
|
+
}
|
|
17
|
+
export function safeError(error) {
|
|
18
|
+
return (error instanceof Error ? error.message : String(error)).replace(/[\r\n]+/g, ' ').slice(0, 1_000);
|
|
19
|
+
}
|
|
20
|
+
export function errorOutput(error) {
|
|
21
|
+
if (!error || typeof error !== 'object')
|
|
22
|
+
return {};
|
|
23
|
+
const output = error.output;
|
|
24
|
+
return isJson(output) ? { output } : {};
|
|
25
|
+
}
|
|
26
|
+
const TRANSIENT_CODES = new Set([
|
|
27
|
+
'ECONNREFUSED',
|
|
28
|
+
'ECONNRESET',
|
|
29
|
+
'EAI_AGAIN',
|
|
30
|
+
'ENETDOWN',
|
|
31
|
+
'ENETUNREACH',
|
|
32
|
+
'ETIMEDOUT',
|
|
33
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
34
|
+
'UND_ERR_HEADERS_TIMEOUT',
|
|
35
|
+
'UND_ERR_SOCKET',
|
|
36
|
+
'internal',
|
|
37
|
+
]);
|
|
38
|
+
function isJson(value) {
|
|
39
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
40
|
+
return true;
|
|
41
|
+
if (typeof value === 'number')
|
|
42
|
+
return Number.isFinite(value);
|
|
43
|
+
if (Array.isArray(value))
|
|
44
|
+
return value.every(isJson);
|
|
45
|
+
return Boolean(value) && typeof value === 'object'
|
|
46
|
+
&& Object.getPrototypeOf(value) === Object.prototype
|
|
47
|
+
&& Object.values(value).every(isJson);
|
|
48
|
+
}
|
package/dist/src/executor.d.ts
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AutomationActionPort, AutomationRunExecutorOptions } from './execution-contract.js';
|
|
2
2
|
import type { ClaimedAutomationRun, MachineRuns } from './machine.js';
|
|
3
3
|
export { automationPlan } from './plan.js';
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
actionId: string;
|
|
7
|
-
payload: Json;
|
|
8
|
-
idempotencyKey: string;
|
|
9
|
-
signal: AbortSignal;
|
|
10
|
-
}>): Promise<Json>;
|
|
11
|
-
}
|
|
12
|
-
export interface AutomationRunExecutorOptions {
|
|
13
|
-
readonly now?: () => Date;
|
|
14
|
-
readonly heartbeatMs?: number;
|
|
15
|
-
readonly maxAttempts?: number;
|
|
16
|
-
readonly retryDelayMs?: (attempt: number) => number;
|
|
17
|
-
}
|
|
4
|
+
export { transientFailure } from './execution-errors.js';
|
|
5
|
+
export type { AutomationActionPort, AutomationProcessExecution, AutomationProcessPort, AutomationRunExecutorOptions, } from './execution-contract.js';
|
|
18
6
|
export declare class AutomationRunExecutor {
|
|
19
7
|
#private;
|
|
20
8
|
private readonly runs;
|
|
@@ -22,4 +10,3 @@ export declare class AutomationRunExecutor {
|
|
|
22
10
|
constructor(runs: MachineRuns, actions: AutomationActionPort, options?: AutomationRunExecutorOptions);
|
|
23
11
|
execute(run: ClaimedAutomationRun): Promise<void>;
|
|
24
12
|
}
|
|
25
|
-
export declare function transientFailure(error: unknown): boolean;
|
package/dist/src/executor.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveInputTemplate } from './input-template.js';
|
|
2
2
|
import { automationPlan } from './plan.js';
|
|
3
|
+
import { errorOutput, safeError, transientFailure } from './execution-errors.js';
|
|
3
4
|
import { completed, emptyJournal, journalJson, putStep, runJournal, stepAttempt, } from './run-journal.js';
|
|
4
5
|
export { automationPlan } from './plan.js';
|
|
6
|
+
export { transientFailure } from './execution-errors.js';
|
|
5
7
|
export class AutomationRunExecutor {
|
|
6
8
|
runs;
|
|
7
9
|
actions;
|
|
@@ -9,6 +11,7 @@ export class AutomationRunExecutor {
|
|
|
9
11
|
#heartbeatMs;
|
|
10
12
|
#maxAttempts;
|
|
11
13
|
#retryDelayMs;
|
|
14
|
+
#processes;
|
|
12
15
|
constructor(runs, actions, options = {}) {
|
|
13
16
|
this.runs = runs;
|
|
14
17
|
this.actions = actions;
|
|
@@ -16,6 +19,7 @@ export class AutomationRunExecutor {
|
|
|
16
19
|
this.#heartbeatMs = integer(options.heartbeatMs ?? 20_000, 1, 'heartbeatMs');
|
|
17
20
|
this.#maxAttempts = integer(options.maxAttempts ?? 5, 1, 'maxAttempts');
|
|
18
21
|
this.#retryDelayMs = options.retryDelayMs ?? defaultRetryDelayMs;
|
|
22
|
+
this.#processes = options.processes;
|
|
19
23
|
}
|
|
20
24
|
async execute(run) {
|
|
21
25
|
const confirmed = await this.runs.update(run.id, {
|
|
@@ -60,6 +64,7 @@ export class AutomationRunExecutor {
|
|
|
60
64
|
finishedAt: this.#now().toISOString(),
|
|
61
65
|
error,
|
|
62
66
|
failureKind,
|
|
67
|
+
...errorOutput(action.error),
|
|
63
68
|
});
|
|
64
69
|
if (failureKind === 'transient' && run.attempts < this.#maxAttempts) {
|
|
65
70
|
await this.runs.update(run.id, {
|
|
@@ -137,12 +142,7 @@ export class AutomationRunExecutor {
|
|
|
137
142
|
const timer = setInterval(renew, this.#heartbeatMs);
|
|
138
143
|
timer.unref?.();
|
|
139
144
|
try {
|
|
140
|
-
const output = await this.
|
|
141
|
-
actionId: step.actionId,
|
|
142
|
-
payload: resolveActionInput(step.input, run.input),
|
|
143
|
-
idempotencyKey: `${run.id}:${step.id}`,
|
|
144
|
-
signal: controller.signal,
|
|
145
|
-
});
|
|
145
|
+
const output = await this.#executeStep(run, step, controller.signal);
|
|
146
146
|
return leaseLost ? { leaseLost: true } : { leaseLost: false, output };
|
|
147
147
|
}
|
|
148
148
|
catch (error) {
|
|
@@ -154,35 +154,30 @@ export class AutomationRunExecutor {
|
|
|
154
154
|
await heartbeat;
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
157
|
+
#executeStep(run, step, signal) {
|
|
158
|
+
const idempotencyKey = `${run.id}:${step.id}`;
|
|
159
|
+
if ('kind' in step) {
|
|
160
|
+
if (!this.#processes) {
|
|
161
|
+
throw Object.assign(new Error('Automation process execution is not installed on this machine'), {
|
|
162
|
+
code: 'process_execution_unavailable',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const { id: _id, stdin, ...execution } = step;
|
|
166
|
+
return this.#processes.execute({
|
|
167
|
+
...execution,
|
|
168
|
+
...(stdin === undefined ? {} : { stdin: processStdin(resolveInputTemplate(stdin, run.input)) }),
|
|
169
|
+
idempotencyKey,
|
|
170
|
+
signal,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return this.actions.call({
|
|
174
|
+
actionId: step.actionId,
|
|
175
|
+
payload: resolveInputTemplate(step.input, run.input),
|
|
176
|
+
idempotencyKey,
|
|
177
|
+
signal,
|
|
178
|
+
});
|
|
170
179
|
}
|
|
171
|
-
const message = safeError(error).toLowerCase();
|
|
172
|
-
return message.includes('fetch failed') || message.includes('network error');
|
|
173
180
|
}
|
|
174
|
-
const TRANSIENT_CODES = new Set([
|
|
175
|
-
'ECONNREFUSED',
|
|
176
|
-
'ECONNRESET',
|
|
177
|
-
'EAI_AGAIN',
|
|
178
|
-
'ENETDOWN',
|
|
179
|
-
'ENETUNREACH',
|
|
180
|
-
'ETIMEDOUT',
|
|
181
|
-
'UND_ERR_CONNECT_TIMEOUT',
|
|
182
|
-
'UND_ERR_HEADERS_TIMEOUT',
|
|
183
|
-
'UND_ERR_SOCKET',
|
|
184
|
-
'internal',
|
|
185
|
-
]);
|
|
186
181
|
function defaultRetryDelayMs(attempt) {
|
|
187
182
|
return Math.min(5 * 60_000, 2_000 * (2 ** Math.max(0, attempt - 1)));
|
|
188
183
|
}
|
|
@@ -191,6 +186,6 @@ function integer(value, minimum, name) {
|
|
|
191
186
|
throw new Error(`${name} must be an integer of at least ${minimum}`);
|
|
192
187
|
return value;
|
|
193
188
|
}
|
|
194
|
-
function
|
|
195
|
-
return
|
|
189
|
+
function processStdin(value) {
|
|
190
|
+
return typeof value === 'string' ? value : `${JSON.stringify(value)}\n`;
|
|
196
191
|
}
|