@amalgm/automations 0.3.2 → 0.4.1
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 +16 -1
- package/PURPOSE.md +30 -0
- package/README.md +53 -4
- package/dist/host/auth.js +4 -1
- package/dist/host/main.js +11 -1
- package/dist/host/notifications.d.ts +4 -0
- package/dist/host/notifications.js +19 -0
- package/dist/host/server.d.ts +1 -0
- package/dist/host/server.js +51 -10
- package/dist/host/skill.d.ts +2 -0
- package/dist/host/skill.js +33 -0
- package/dist/skills/amalgm-automations.5e4e14f0ace632c8383cd014e1f0f34b24ec0d5c5600132a840abc4c313b31d4.tgz +0 -0
- package/dist/skills/index.json +1 -0
- package/dist/src/cli/run.d.ts +1 -1
- package/dist/src/cli/run.js +17 -0
- package/dist/src/crud/triggers.js +0 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/machine-client.d.ts +3 -1
- package/dist/src/machine-client.js +6 -0
- package/dist/src/machine-event-stream.d.ts +8 -0
- package/dist/src/machine-event-stream.js +58 -0
- package/dist/src/machine-http.d.ts +2 -0
- package/dist/src/machine-http.js +8 -2
- package/dist/src/machine-notifications.d.ts +15 -0
- package/dist/src/machine-notifications.js +124 -0
- package/dist/src/machine.d.ts +1 -0
- package/dist/src/mcp.d.ts +2 -1
- package/dist/src/mcp.js +4 -2
- package/dist/src/runner.d.ts +3 -1
- package/dist/src/runner.js +69 -46
- package/dist/src/schema.js +9 -3
- package/dist/src/supabase-machine.d.ts +1 -0
- package/dist/src/supabase-machine.js +3 -0
- package/dist/src/tool-surface.js +21 -2
- package/package.json +3 -2
- package/skills/amalgm-automations/SKILL.md +49 -11
- package/skills/amalgm-automations/agents/openai.yaml +2 -2
- package/skills/amalgm-automations/references/command-contract.md +7 -0
- package/skills/amalgm-automations/references/setup-and-support.md +236 -0
- package/supabase/migrations/20260904020000_machine_run_notifications.sql +52 -0
- package/supabase/migrations/20260904030000_machine_notification_permissions.sql +4 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { AutomationError } from './errors.js';
|
|
2
|
+
/** Ephemeral wakeups only. Supabase remains the queue and the eligibility clock. */
|
|
3
|
+
export function createMachineRunNotifications(options) {
|
|
4
|
+
const targets = new Map();
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
let connected = false;
|
|
7
|
+
const key = (userId, computerId) => JSON.stringify([userId, computerId]);
|
|
8
|
+
const refresh = async (target) => {
|
|
9
|
+
target.dirty = true;
|
|
10
|
+
if (target.refreshing)
|
|
11
|
+
return;
|
|
12
|
+
target.refreshing = true;
|
|
13
|
+
try {
|
|
14
|
+
while (target.dirty && target.listeners.size) {
|
|
15
|
+
target.dirty = false;
|
|
16
|
+
clearTimeout(target.timer);
|
|
17
|
+
const delay = await options.wakeDelay(target.userId, target.computerId);
|
|
18
|
+
if (target.dirty || !target.listeners.size)
|
|
19
|
+
continue;
|
|
20
|
+
if (delay === null)
|
|
21
|
+
continue;
|
|
22
|
+
if (!Number.isFinite(delay) || delay < 0)
|
|
23
|
+
throw new Error('Invalid machine wake delay');
|
|
24
|
+
if (delay === 0) {
|
|
25
|
+
for (const listener of target.listeners)
|
|
26
|
+
listener.send('wake');
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
target.timer = setTimeout(() => void refresh(target), Math.min(delay, 2_147_483_647));
|
|
30
|
+
target.timer.unref();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
options.log?.('automations.notifications.failed', { boundary: 'wake-deadline' });
|
|
36
|
+
for (const listener of [...target.listeners])
|
|
37
|
+
listener.close();
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
target.refreshing = false;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const disconnect = () => {
|
|
44
|
+
connected = false;
|
|
45
|
+
for (const target of targets.values()) {
|
|
46
|
+
for (const listener of [...target.listeners])
|
|
47
|
+
listener.close();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
connected() { connected = true; },
|
|
52
|
+
disconnected: disconnect,
|
|
53
|
+
close: disconnect,
|
|
54
|
+
changed(userId, computerId) {
|
|
55
|
+
const target = targets.get(key(userId, computerId));
|
|
56
|
+
if (target)
|
|
57
|
+
void refresh(target);
|
|
58
|
+
},
|
|
59
|
+
open(principal, signal) {
|
|
60
|
+
if (!connected)
|
|
61
|
+
throw new AutomationError('unavailable', 'Notifications are reconnecting', 503);
|
|
62
|
+
signal.throwIfAborted();
|
|
63
|
+
const targetKey = key(principal.userId, principal.computerId);
|
|
64
|
+
let target = targets.get(targetKey);
|
|
65
|
+
if (!target) {
|
|
66
|
+
target = { ...principal, listeners: new Set(), refreshing: false, dirty: false };
|
|
67
|
+
targets.set(targetKey, target);
|
|
68
|
+
}
|
|
69
|
+
const current = target;
|
|
70
|
+
let close = (_closeStream = true) => { };
|
|
71
|
+
const body = new ReadableStream({
|
|
72
|
+
start(controller) {
|
|
73
|
+
let closed = false;
|
|
74
|
+
let heartbeat;
|
|
75
|
+
let expiry;
|
|
76
|
+
const abort = () => close();
|
|
77
|
+
close = (closeStream = true) => {
|
|
78
|
+
if (closed)
|
|
79
|
+
return;
|
|
80
|
+
closed = true;
|
|
81
|
+
clearInterval(heartbeat);
|
|
82
|
+
clearTimeout(expiry);
|
|
83
|
+
signal.removeEventListener('abort', abort);
|
|
84
|
+
current.listeners.delete(listener);
|
|
85
|
+
if (!current.listeners.size) {
|
|
86
|
+
clearTimeout(current.timer);
|
|
87
|
+
targets.delete(targetKey);
|
|
88
|
+
}
|
|
89
|
+
if (closeStream)
|
|
90
|
+
controller.close();
|
|
91
|
+
};
|
|
92
|
+
const listener = {
|
|
93
|
+
send(event) {
|
|
94
|
+
if (closed)
|
|
95
|
+
return;
|
|
96
|
+
if ((controller.desiredSize ?? 0) <= 0)
|
|
97
|
+
return close();
|
|
98
|
+
controller.enqueue(encoder.encode(event === 'heartbeat'
|
|
99
|
+
? ': keepalive\n\n' : `event: ${event}\ndata: {}\n\n`));
|
|
100
|
+
},
|
|
101
|
+
close,
|
|
102
|
+
};
|
|
103
|
+
current.listeners.add(listener);
|
|
104
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
105
|
+
heartbeat = setInterval(() => listener.send('heartbeat'), options.heartbeatMs ?? 30_000);
|
|
106
|
+
heartbeat.unref();
|
|
107
|
+
const lifetime = Math.min(options.maxStreamMs ?? 300_000, (principal.authorizationExpiresAt ?? Infinity) - Date.now());
|
|
108
|
+
expiry = setTimeout(close, Math.max(0, lifetime));
|
|
109
|
+
expiry.unref();
|
|
110
|
+
listener.send('ready');
|
|
111
|
+
void refresh(current);
|
|
112
|
+
},
|
|
113
|
+
cancel() { close(false); },
|
|
114
|
+
}, { highWaterMark: 8 });
|
|
115
|
+
return new Response(body, {
|
|
116
|
+
headers: {
|
|
117
|
+
'content-type': 'text/event-stream',
|
|
118
|
+
'cache-control': 'no-store, no-transform',
|
|
119
|
+
'x-accel-buffering': 'no',
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
package/dist/src/machine.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export interface AutomationMachinePrincipal {
|
|
|
4
4
|
readonly userId: string;
|
|
5
5
|
readonly computerId: string;
|
|
6
6
|
readonly scopes: readonly AutomationScope[];
|
|
7
|
+
readonly authorizationExpiresAt?: number;
|
|
7
8
|
}
|
|
8
9
|
export interface ClaimedAutomationRun extends AutomationRun {
|
|
9
10
|
readonly leaseToken: string;
|
package/dist/src/mcp.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { type AutomationCommands } from './command-surface.js';
|
|
2
3
|
import type { AutomationCrud } from './contract.js';
|
|
3
4
|
import { createAutomationToolSurface } from './tool-surface.js';
|
|
4
5
|
export { createAutomationToolSurface };
|
|
5
6
|
export type { AutomationToolSurface, AutomationView, CreateAutomationDefinition, UpdateAutomationDefinition, WorkflowChange, } from './tool-surface.js';
|
|
6
|
-
export declare function createAutomationMcpServer(sdk: AutomationCrud): McpServer;
|
|
7
|
+
export declare function createAutomationMcpServer(sdk: AutomationCrud | AutomationCommands): McpServer;
|
package/dist/src/mcp.js
CHANGED
|
@@ -14,7 +14,7 @@ const resultSchema = {
|
|
|
14
14
|
};
|
|
15
15
|
export function createAutomationMcpServer(sdk) {
|
|
16
16
|
const server = new McpServer({ name: 'amalgm-automations-mcp-server', version: '0.1.0' });
|
|
17
|
-
const commands = createAutomationCommands(sdk);
|
|
17
|
+
const commands = 'execute' in sdk ? sdk : createAutomationCommands(sdk);
|
|
18
18
|
for (const definition of automationCommandDefinitions)
|
|
19
19
|
register(server, commands, definition);
|
|
20
20
|
return server;
|
|
@@ -23,7 +23,9 @@ function register(server, commands, definition) {
|
|
|
23
23
|
server.registerTool(definition.mcpName, {
|
|
24
24
|
title: definition.title,
|
|
25
25
|
description: definition.description,
|
|
26
|
-
|
|
26
|
+
// Forward unknown top-level fields to the SDK's strict parser. The MCP
|
|
27
|
+
// SDK's default object parser strips them before our command sees them.
|
|
28
|
+
inputSchema: z.object(definition.inputShape).passthrough(),
|
|
27
29
|
outputSchema: resultSchema,
|
|
28
30
|
annotations: definition.annotations,
|
|
29
31
|
}, async (input) => {
|
package/dist/src/runner.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type { ClaimedAutomationRun, MachineRuns } from './machine.js';
|
|
2
2
|
export interface AutomationMachineRunnerOptions {
|
|
3
3
|
readonly runs: MachineRuns;
|
|
4
|
+
readonly notifications: (signal: AbortSignal) => AsyncIterable<void>;
|
|
4
5
|
readonly execute: (run: ClaimedAutomationRun) => Promise<void>;
|
|
5
6
|
readonly batchSize?: number;
|
|
6
7
|
readonly leaseSeconds?: number;
|
|
7
|
-
readonly
|
|
8
|
+
readonly reconnectDelayMs?: number;
|
|
8
9
|
readonly maxBackoffMs?: number;
|
|
9
10
|
readonly log?: (event: string, details?: Readonly<Record<string, unknown>>) => void;
|
|
10
11
|
}
|
|
12
|
+
/** Listen first, then drain. There is no timer that asks an idle machine for work. */
|
|
11
13
|
export declare function createAutomationMachineRunner(options: AutomationMachineRunnerOptions): Readonly<{
|
|
12
14
|
start(): void;
|
|
13
15
|
close(): Promise<void>;
|
package/dist/src/runner.js
CHANGED
|
@@ -1,61 +1,84 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
/** Listen first, then drain. There is no timer that asks an idle machine for work. */
|
|
1
3
|
export function createAutomationMachineRunner(options) {
|
|
2
4
|
const batchSize = integer(options.batchSize ?? 8, 1, 20, 'batchSize');
|
|
3
5
|
const leaseSeconds = integer(options.leaseSeconds ?? 60, 30, 900, 'leaseSeconds');
|
|
4
|
-
const
|
|
5
|
-
const maxBackoffMs = integer(options.maxBackoffMs ??
|
|
6
|
-
let
|
|
7
|
-
let
|
|
8
|
-
let
|
|
9
|
-
let
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (
|
|
6
|
+
const reconnectDelayMs = integer(options.reconnectDelayMs ?? 1_000, 1, 60_000, 'reconnectDelayMs');
|
|
7
|
+
const maxBackoffMs = integer(options.maxBackoffMs ?? 30_000, reconnectDelayMs, 300_000, 'maxBackoffMs');
|
|
8
|
+
let lifetime = null;
|
|
9
|
+
let listening = null;
|
|
10
|
+
let draining = null;
|
|
11
|
+
let pending = false;
|
|
12
|
+
const wake = (connection, signal) => {
|
|
13
|
+
pending = true;
|
|
14
|
+
if (draining)
|
|
13
15
|
return;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
options.log?.('automations.claimed', { count: runs.length });
|
|
25
|
-
const settled = await Promise.allSettled(runs.map(options.execute));
|
|
26
|
-
settled.forEach((result) => {
|
|
27
|
-
if (result.status === 'rejected') {
|
|
28
|
-
options.log?.('automations.run.failed', { error: safe(result.reason) });
|
|
16
|
+
draining = (async () => {
|
|
17
|
+
while (pending && !signal.aborted) {
|
|
18
|
+
pending = false;
|
|
19
|
+
const runs = await options.runs.claim({ limit: batchSize, leaseSeconds });
|
|
20
|
+
if (runs.length)
|
|
21
|
+
options.log?.('automations.claimed', { count: runs.length });
|
|
22
|
+
const settled = await Promise.allSettled(runs.map(options.execute));
|
|
23
|
+
for (const result of settled) {
|
|
24
|
+
if (result.status === 'rejected')
|
|
25
|
+
options.log?.('automations.run.failed', { error: safe(result.reason) });
|
|
29
26
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
active = null;
|
|
41
|
-
schedule(nextDelay);
|
|
27
|
+
// Work may arrive while execution is busy, including a partial last batch.
|
|
28
|
+
pending ||= runs.length > 0;
|
|
29
|
+
}
|
|
30
|
+
})().catch((error) => {
|
|
31
|
+
options.log?.('automations.claim.failed', { error: safe(error) });
|
|
32
|
+
connection.abort(error);
|
|
33
|
+
}).finally(() => {
|
|
34
|
+
draining = null;
|
|
35
|
+
if (pending && !signal.aborted && !connection.signal.aborted)
|
|
36
|
+
wake(connection, signal);
|
|
42
37
|
});
|
|
43
|
-
|
|
38
|
+
};
|
|
39
|
+
const listen = async (signal) => {
|
|
40
|
+
let failures = 0;
|
|
41
|
+
while (!signal.aborted) {
|
|
42
|
+
const connection = new AbortController();
|
|
43
|
+
const connectedSignal = AbortSignal.any([signal, connection.signal]);
|
|
44
|
+
const startedAt = Date.now();
|
|
45
|
+
try {
|
|
46
|
+
for await (const _ of options.notifications(connectedSignal)) {
|
|
47
|
+
if (connectedSignal.aborted)
|
|
48
|
+
break;
|
|
49
|
+
wake(connection, signal);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (!signal.aborted)
|
|
54
|
+
options.log?.('automations.notifications.disconnected', { error: safe(error) });
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
connection.abort();
|
|
58
|
+
await draining;
|
|
59
|
+
}
|
|
60
|
+
if (signal.aborted)
|
|
61
|
+
break;
|
|
62
|
+
// A flapping stream must not reset its backoff merely by sending ready.
|
|
63
|
+
failures = Date.now() - startedAt >= 30_000 ? 0 : failures + 1;
|
|
64
|
+
const backoff = Math.min(maxBackoffMs, reconnectDelayMs * 2 ** Math.min(Math.max(0, failures - 1), 10));
|
|
65
|
+
await delay(backoff, undefined, { signal, ref: false }).catch(() => { });
|
|
66
|
+
}
|
|
44
67
|
};
|
|
45
68
|
return Object.freeze({
|
|
46
69
|
start() {
|
|
47
|
-
if (
|
|
70
|
+
if (lifetime)
|
|
48
71
|
return;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
72
|
+
lifetime = new AbortController();
|
|
73
|
+
pending = false;
|
|
74
|
+
listening = listen(lifetime.signal);
|
|
52
75
|
},
|
|
53
76
|
async close() {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
lifetime?.abort();
|
|
78
|
+
await listening;
|
|
79
|
+
await draining;
|
|
80
|
+
lifetime = null;
|
|
81
|
+
listening = null;
|
|
59
82
|
},
|
|
60
83
|
});
|
|
61
84
|
}
|
package/dist/src/schema.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod/v3';
|
|
2
2
|
import { ValidationError } from './errors.js';
|
|
3
3
|
import { compiledAutomationPlan } from './plan.js';
|
|
4
|
-
import { IDENTIFIER } from './validation.js';
|
|
4
|
+
import { IDENTIFIER, schedule } from './validation.js';
|
|
5
5
|
const maxPageSize = 100;
|
|
6
6
|
const identifierMessage = 'Identifier is invalid';
|
|
7
7
|
const text = (maximum, label) => z.string()
|
|
@@ -96,10 +96,16 @@ export function parseListAutomations(value) {
|
|
|
96
96
|
return parse(listAutomationsSchema, value);
|
|
97
97
|
}
|
|
98
98
|
export function parseCreateScheduleTrigger(value) {
|
|
99
|
-
|
|
99
|
+
const trigger = parse(createScheduleTriggerSchema, value);
|
|
100
|
+
schedule(trigger.cron, trigger.timezone ?? 'UTC');
|
|
101
|
+
return trigger;
|
|
100
102
|
}
|
|
101
103
|
export function parseUpdateScheduleTrigger(value) {
|
|
102
|
-
|
|
104
|
+
const patch = parse(updateScheduleTriggerSchema, value);
|
|
105
|
+
if (patch.cron !== undefined || patch.timezone !== undefined) {
|
|
106
|
+
schedule(patch.cron ?? '* * * * *', patch.timezone ?? 'UTC');
|
|
107
|
+
}
|
|
108
|
+
return patch;
|
|
103
109
|
}
|
|
104
110
|
export function parseCreateWebhookTrigger(value) {
|
|
105
111
|
return parse(createWebhookTriggerSchema, value);
|
|
@@ -11,6 +11,7 @@ export declare class SupabaseMachineRunRepository implements MachineRunRepositor
|
|
|
11
11
|
#private;
|
|
12
12
|
private readonly client;
|
|
13
13
|
constructor(client: MachineRpcClient);
|
|
14
|
+
wakeDelay(userId: string, computerId: string): Promise<number | null>;
|
|
14
15
|
claim(input: Parameters<MachineRunRepository['claim']>[0]): Promise<ClaimedAutomationRun[]>;
|
|
15
16
|
update(input: Parameters<MachineRunRepository['update']>[0]): Promise<ClaimedAutomationRun | null>;
|
|
16
17
|
}
|
|
@@ -4,6 +4,9 @@ export class SupabaseMachineRunRepository {
|
|
|
4
4
|
constructor(client) {
|
|
5
5
|
this.client = client;
|
|
6
6
|
}
|
|
7
|
+
wakeDelay(userId, computerId) {
|
|
8
|
+
return this.#call('amalgm_machine_wake_delay', { p_user_id: userId, p_target_id: computerId });
|
|
9
|
+
}
|
|
7
10
|
async claim(input) {
|
|
8
11
|
const rows = await this.#call('claim_amalgm_machine_runs', {
|
|
9
12
|
p_user_id: input.userId,
|
package/dist/src/tool-surface.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { NotFoundError, ValidationError } from './errors.js';
|
|
1
|
+
import { AutomationError, NotFoundError, ValidationError } from './errors.js';
|
|
2
|
+
import { parseCreateAutomation, parseCreateScheduleTrigger, parseCreateWebhookTrigger, parseCreateWorkflow, parseUpdateAutomation, parseUpdateScheduleTrigger, parseUpdateWebhookTrigger, parseUpdateWorkflow, } from './schema.js';
|
|
2
3
|
export function createAutomationToolSurface(sdk) {
|
|
3
4
|
const view = async (automationId, runs = false) => {
|
|
4
5
|
const automation = await sdk.automations.get(automationId);
|
|
@@ -14,6 +15,11 @@ export function createAutomationToolSurface(sdk) {
|
|
|
14
15
|
const surface = {
|
|
15
16
|
async create(input) {
|
|
16
17
|
const { schedules = [], webhooks = [], workflow, ...automationInput } = input;
|
|
18
|
+
parseCreateAutomation(automationInput);
|
|
19
|
+
schedules.forEach(parseCreateScheduleTrigger);
|
|
20
|
+
webhooks.forEach(parseCreateWebhookTrigger);
|
|
21
|
+
if (workflow)
|
|
22
|
+
parseCreateWorkflow(workflow);
|
|
17
23
|
validateCreateIds(schedules, webhooks);
|
|
18
24
|
const staged = schedules.length > 0 || webhooks.length > 0 || workflow !== undefined;
|
|
19
25
|
const requestedEnabled = automationInput.enabled !== false;
|
|
@@ -40,6 +46,7 @@ export function createAutomationToolSurface(sdk) {
|
|
|
40
46
|
get: view,
|
|
41
47
|
async update(automationId, input) {
|
|
42
48
|
requireChanges(input);
|
|
49
|
+
validateUpdate(input);
|
|
43
50
|
validateChanges(input.schedules, 'schedule');
|
|
44
51
|
validateChanges(input.webhooks, 'webhook');
|
|
45
52
|
const current = await sdk.automations.get(automationId);
|
|
@@ -98,6 +105,18 @@ function requireChanges(input) {
|
|
|
98
105
|
throw new ValidationError('Automation update must include a change');
|
|
99
106
|
}
|
|
100
107
|
}
|
|
108
|
+
function validateUpdate(input) {
|
|
109
|
+
if (input.patch)
|
|
110
|
+
parseUpdateAutomation(input.patch);
|
|
111
|
+
input.schedules?.create?.forEach(parseCreateScheduleTrigger);
|
|
112
|
+
input.schedules?.update?.forEach(({ patch }) => parseUpdateScheduleTrigger(patch));
|
|
113
|
+
input.webhooks?.create?.forEach(parseCreateWebhookTrigger);
|
|
114
|
+
input.webhooks?.update?.forEach(({ patch }) => parseUpdateWebhookTrigger(patch));
|
|
115
|
+
if (input.workflow && 'create' in input.workflow)
|
|
116
|
+
parseCreateWorkflow(input.workflow.create);
|
|
117
|
+
if (input.workflow && 'update' in input.workflow)
|
|
118
|
+
parseUpdateWorkflow(input.workflow.update);
|
|
119
|
+
}
|
|
101
120
|
function hasChanges(changes) {
|
|
102
121
|
return Boolean(changes && [changes.create, changes.update, changes.delete]
|
|
103
122
|
.some((items) => items && items.length > 0));
|
|
@@ -122,5 +141,5 @@ function validateCreateIds(schedules, webhooks) {
|
|
|
122
141
|
}
|
|
123
142
|
function disabledDraftError(automationId, error) {
|
|
124
143
|
const reason = error instanceof Error ? error.message : String(error);
|
|
125
|
-
return new
|
|
144
|
+
return new AutomationError(error instanceof AutomationError ? error.code : 'internal', `Automation ${automationId} remains disabled because configuration failed: ${reason}`, error instanceof AutomationError ? error.status : undefined);
|
|
126
145
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amalgm/automations",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Amalgm's automation SDK: durable trigger admission and target-machine execution.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -45,10 +45,11 @@
|
|
|
45
45
|
"README.md"
|
|
46
46
|
],
|
|
47
47
|
"scripts": {
|
|
48
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json && tsx scripts/mark-executables.ts",
|
|
48
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && tsx scripts/mark-executables.ts && tsx scripts/build-public-skill.ts",
|
|
49
49
|
"check": "tsx scripts/check-tree.ts && tsc -p tsconfig.json --noEmit",
|
|
50
50
|
"test": "tsx --test test/*.test.ts",
|
|
51
51
|
"test:supabase": "tsx --test test/integration/postgres.test.ts",
|
|
52
|
+
"test:notifications": "tsx scripts/test-notifications.ts",
|
|
52
53
|
"verify": "npm run check && npm test && npm run build && tsx scripts/check-cli-artifact.ts",
|
|
53
54
|
"release:check": "npm run verify && npm run test:supabase",
|
|
54
55
|
"prepack": "npm run build",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: amalgm-automations
|
|
3
|
-
description:
|
|
3
|
+
description: Set up, operate, and troubleshoot Amalgm Automations through MCP or the amalgm automations CLI. Use for installation, Google sign-in, scheduled or webhook workflows, Run Now, run history, and Automations support.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Amalgm Automations
|
|
@@ -10,6 +10,10 @@ workflow into one durable Amalgm automation and verify what actually ran.
|
|
|
10
10
|
Automations owns configuration and permanent run history; the selected Amalgm
|
|
11
11
|
machine executes the workflow.
|
|
12
12
|
|
|
13
|
+
Amalgm Automations is free. Guide the person through Google sign-in and computer
|
|
14
|
+
approval when needed; do not introduce a payment step. A native agent or other
|
|
15
|
+
external service still uses its own account and may have its own usage costs.
|
|
16
|
+
|
|
13
17
|
## Choose the available adapter
|
|
14
18
|
|
|
15
19
|
Prefer the six `amalgm_automations_*` MCP tools when they are callable. In a
|
|
@@ -28,11 +32,22 @@ amalgm_automations_run_now amalgm automations run-now
|
|
|
28
32
|
Both adapters accept the same JSON object and call the same SDK command
|
|
29
33
|
surface. Do not translate one into a different resource-level API.
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
Reuse a working connection. For an available MCP connection, a small `list`
|
|
36
|
+
request with `{"limit":1}` proves access; do not install or log in again. For
|
|
37
|
+
CLI use, check `amalgm automations --help` and use
|
|
38
|
+
`amalgm automations list --input '{"limit":1}'`. An empty successful list is
|
|
39
|
+
ready, not a setup failure. Respect an explicitly selected account.
|
|
40
|
+
|
|
41
|
+
If the user asks to install or sign in, or that read fails, read
|
|
42
|
+
[setup and support](references/setup-and-support.md). It covers the supported
|
|
43
|
+
install, browser approval, account selection, runtime restart, and recovery
|
|
44
|
+
commands. Carry setup through to a successful Automations read, then resume
|
|
45
|
+
the original request. Do not stop at “Shell must be running.”
|
|
46
|
+
|
|
47
|
+
Shell owns authentication. Never read, copy, or ask the user to supply a runtime
|
|
48
|
+
token, DPoP proof, device key, or machine credential. The public connection
|
|
49
|
+
commands obtain what they need; environment-variable credential setup is not
|
|
50
|
+
the user login flow.
|
|
36
51
|
|
|
37
52
|
Read [references/command-contract.md](references/command-contract.md) whenever
|
|
38
53
|
composing `create` or `update`, or when exact fields are uncertain. Read
|
|
@@ -57,13 +72,16 @@ input.
|
|
|
57
72
|
complex source. Do not expose those values in command arguments or logs.
|
|
58
73
|
- Delete only after the exact id is established and deletion is within the
|
|
59
74
|
user's request. Deleting configuration intentionally retains run history.
|
|
75
|
+
Omit `id` when creating a new automation; explicitly reusing a deleted id
|
|
76
|
+
refers to the same logical identity and retains its history and retry keys.
|
|
60
77
|
|
|
61
78
|
## Build one complete definition
|
|
62
79
|
|
|
63
80
|
Create the automation, its schedule/webhook triggers, and its optional workflow
|
|
64
|
-
in one task-level call.
|
|
65
|
-
|
|
66
|
-
|
|
81
|
+
in one task-level call. Invalid supplied configuration fails before any write.
|
|
82
|
+
The service stages valid multi-resource changes and enables the definition
|
|
83
|
+
only after setup succeeds. If a later service failure returns a disabled draft
|
|
84
|
+
id, get and repair that draft with `update`; do not create a replacement.
|
|
67
85
|
|
|
68
86
|
Use a finite `maxOccurrences` when the user's request is bounded. Do not turn
|
|
69
87
|
"ten times" into an unbounded schedule plus a future cleanup promise.
|
|
@@ -87,27 +105,47 @@ executables through the selected machine; never guess either.
|
|
|
87
105
|
not evidence that work completed.
|
|
88
106
|
|
|
89
107
|
1. Supply a stable, caller-chosen `idempotency_key` whenever the request might
|
|
90
|
-
be retried.
|
|
108
|
+
be retried. Use a new key for each new logical run.
|
|
91
109
|
2. If admission is retried, send the same automation id, input, and key. The
|
|
92
110
|
returned run id must remain the same.
|
|
93
111
|
3. Poll `get` with `include_runs: true` or a `run_query` until the admitted run
|
|
94
112
|
becomes `completed` or `failed`. Match the exact run id; do not assume the
|
|
95
|
-
newest unrelated run is yours.
|
|
113
|
+
newest unrelated run is yours. Space out reads and back off while state is
|
|
114
|
+
unchanged; a rate-limit response is a reason to wait, not to submit again.
|
|
96
115
|
4. Inspect every step's status and bounded output. Report failure honestly,
|
|
97
116
|
including the failing step and service error.
|
|
98
117
|
|
|
99
118
|
`pending` means durable and awaiting its machine. `sent` or `running` means the
|
|
100
119
|
machine holds a lease. Only `completed` and `failed` are terminal.
|
|
101
120
|
|
|
121
|
+
Keep the user informed while waiting. If the machine stays unavailable or the
|
|
122
|
+
current session cannot keep waiting, report the exact run id and observed
|
|
123
|
+
nonterminal state; do not claim completion, create a replacement, or promise
|
|
124
|
+
background monitoring that has not been set up.
|
|
125
|
+
|
|
102
126
|
Diagnose at the failing boundary. Examples: `process_execution_unavailable`
|
|
103
127
|
means that machine lacks the process host; an executable-not-found error is a
|
|
104
128
|
machine PATH/install problem; `Installed agent has no model` is agent
|
|
105
129
|
configuration; an action-specific HTTP error belongs to that action service.
|
|
106
130
|
Do not relabel these as Run Now failures or patch the automation around them.
|
|
107
131
|
|
|
132
|
+
## Help when something goes wrong
|
|
133
|
+
|
|
134
|
+
Use [setup and support](references/setup-and-support.md) for login/runtime
|
|
135
|
+
failures, HTTP 429 or reported rate limits, and contacting support. Preserve
|
|
136
|
+
the actual error code, distinguish the failing service, and use bounded retries.
|
|
137
|
+
Do not invent minute/hour allowances or reset times that the service has not
|
|
138
|
+
reported.
|
|
139
|
+
|
|
140
|
+
For an unresolved issue, offer **Email Aayush at aayush@amalgm.ai** and prepare
|
|
141
|
+
a concise, redacted draft when helpful. Send only with the user's explicit
|
|
142
|
+
authorization and an available email tool; otherwise give them the draft.
|
|
143
|
+
|
|
108
144
|
## Report the outcome
|
|
109
145
|
|
|
110
146
|
Return the automation id, target choice if the user selected one, trigger
|
|
111
147
|
summary, workflow lanes, and—when execution was requested—the exact run id and
|
|
112
148
|
terminal result. Mention that run history is durable. Never claim a schedule
|
|
113
149
|
fired merely because configuration creation succeeded.
|
|
150
|
+
Include automation/run links when returned by the product; do not invent a
|
|
151
|
+
deep link or use a private webhook URL as a viewing link.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
interface:
|
|
2
2
|
display_name: "Amalgm Automations"
|
|
3
|
-
short_description: "
|
|
4
|
-
default_prompt: "Use $amalgm-automations to create and verify an automation
|
|
3
|
+
short_description: "Set up, run, and troubleshoot Amalgm automations"
|
|
4
|
+
default_prompt: "Use $amalgm-automations to help me connect my account and create and verify an automation."
|
|
@@ -15,6 +15,8 @@ Failures use stderr, a nonzero exit, and:
|
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Do not scrape prose or infer success from exit code alone; parse the envelope.
|
|
18
|
+
Unknown configuration fields are rejected. Arbitrary JSON inside workflow
|
|
19
|
+
action input and Run Now input remains valid payload data.
|
|
18
20
|
|
|
19
21
|
## create
|
|
20
22
|
|
|
@@ -62,6 +64,11 @@ At least one executable step is required when `compiled` is supplied. Omit
|
|
|
62
64
|
webhook URL is the provider destination; a configured signing secret is
|
|
63
65
|
write-only on later reads.
|
|
64
66
|
|
|
67
|
+
Omit `id` for a new automation. An explicit id names a persistent logical
|
|
68
|
+
identity: deleting and recreating it retains that identity's run history and
|
|
69
|
+
Run Now idempotency keys. Use a new key for each new logical run and reuse it
|
|
70
|
+
only when retrying that run.
|
|
71
|
+
|
|
65
72
|
CLI example:
|
|
66
73
|
|
|
67
74
|
```bash
|