@amalgm/automations 0.2.5 → 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.
Files changed (44) hide show
  1. package/AXIOMS.md +52 -12
  2. package/PURPOSE.md +50 -15
  3. package/README.md +119 -14
  4. package/dist/src/automations.js +6 -0
  5. package/dist/src/cli/arguments.d.ts +20 -0
  6. package/dist/src/cli/arguments.js +49 -0
  7. package/dist/src/cli/input.d.ts +6 -0
  8. package/dist/src/cli/input.js +48 -0
  9. package/dist/src/cli/main.d.ts +7 -0
  10. package/dist/src/cli/main.js +39 -0
  11. package/dist/src/cli/run.d.ts +13 -0
  12. package/dist/src/cli/run.js +51 -0
  13. package/dist/src/cli/shell-runtime.d.ts +9 -0
  14. package/dist/src/cli/shell-runtime.js +102 -0
  15. package/dist/src/cli-main.js +2 -20
  16. package/dist/src/cli.d.ts +3 -10
  17. package/dist/src/cli.js +3 -186
  18. package/dist/src/command-result.d.ts +8 -0
  19. package/dist/src/command-result.js +13 -0
  20. package/dist/src/command-surface.d.ts +25 -0
  21. package/dist/src/command-surface.js +125 -0
  22. package/dist/src/contract.d.ts +1 -1
  23. package/dist/src/execution-contract.d.ts +28 -0
  24. package/dist/src/execution-contract.js +1 -0
  25. package/dist/src/execution-errors.d.ts +6 -0
  26. package/dist/src/execution-errors.js +48 -0
  27. package/dist/src/executor.d.ts +3 -16
  28. package/dist/src/executor.js +31 -36
  29. package/dist/src/index.d.ts +7 -3
  30. package/dist/src/index.js +6 -2
  31. package/dist/src/input-template.d.ts +2 -2
  32. package/dist/src/input-template.js +4 -4
  33. package/dist/src/mcp-main.js +0 -0
  34. package/dist/src/mcp.js +15 -79
  35. package/dist/src/node-process-host.d.ts +13 -0
  36. package/dist/src/node-process-host.js +116 -0
  37. package/dist/src/node-process-runner.d.ts +23 -0
  38. package/dist/src/node-process-runner.js +140 -0
  39. package/dist/src/plan.js +125 -9
  40. package/dist/src/run-contract.d.ts +41 -1
  41. package/dist/src/run-journal.d.ts +2 -2
  42. package/dist/src/schema.d.ts +4 -4
  43. package/package.json +3 -3
  44. package/skills/automations/SKILL.md +69 -6
package/dist/src/plan.js CHANGED
@@ -1,4 +1,17 @@
1
1
  import { ValidationError } from './errors.js';
2
+ const MAX_STEPS = 100;
3
+ const MAX_ARGUMENTS = 1_000;
4
+ const MAX_ARGUMENT_LENGTH = 100_000;
5
+ const MAX_SOURCE_LENGTH = 1_000_000;
6
+ const MAX_TIMEOUT_MS = 86_400_000;
7
+ const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
8
+ const RESERVED_PROCESS_ENV = new Set([
9
+ 'AMALGM_AUTOMATION_IDEMPOTENCY_KEY',
10
+ 'AMALGM_AUTOMATIONS_AUTHORIZATION',
11
+ 'AMALGM_COMPUTER_AUTH_TOKEN',
12
+ 'AMALGM_RUNTIME_TOKEN',
13
+ 'AMALGM_TUNNEL_TOKEN',
14
+ ]);
2
15
  export function automationPlan(snapshot) {
3
16
  const root = record(snapshot, 'Automation snapshot');
4
17
  const workflow = record(root.workflow, 'Workflow snapshot');
@@ -6,28 +19,72 @@ export function automationPlan(snapshot) {
6
19
  }
7
20
  export function compiledAutomationPlan(value) {
8
21
  const compiled = record(value, 'Compiled workflow');
9
- if (compiled.version !== 1 || !Array.isArray(compiled.steps)
10
- || compiled.steps.length < 1 || compiled.steps.length > 100) {
11
- throw new ValidationError('Compiled workflow must be a version 1 plan with 1 to 100 steps');
22
+ const version = compiled.version;
23
+ if ((version !== 1 && version !== 2) || !Array.isArray(compiled.steps)
24
+ || compiled.steps.length < 1 || compiled.steps.length > MAX_STEPS) {
25
+ throw new ValidationError(`Compiled workflow must be a version 1 or 2 plan with 1 to ${MAX_STEPS} steps`);
12
26
  }
13
- const steps = compiled.steps.map((item, index) => step(item, index));
27
+ const steps = compiled.steps.map((item, index) => step(item, index, version));
14
28
  const ids = new Set();
15
29
  for (const item of steps) {
16
30
  if (ids.has(item.id))
17
31
  throw new ValidationError(`Workflow step id is duplicated: ${item.id}`);
18
32
  ids.add(item.id);
19
33
  }
20
- return { version: 1, steps };
34
+ return version === 1
35
+ ? { version: 1, steps: steps }
36
+ : { version: 2, steps };
21
37
  }
22
- function step(value, index) {
38
+ function step(value, index, version) {
23
39
  const item = record(value, `Workflow step ${index + 1}`);
24
- if (typeof item.id !== 'string' || !item.id.trim())
25
- throw new ValidationError(`Workflow step ${index + 1} needs an id`);
40
+ const id = requiredText(item.id, `Workflow step ${index + 1} needs an id`, 500);
41
+ if (version === 1 && item.kind !== undefined) {
42
+ throw new ValidationError(`Workflow step ${index + 1} requires plan version 2`);
43
+ }
44
+ if (item.kind === 'command')
45
+ return commandStep(item, id, index);
46
+ if (item.kind === 'script')
47
+ return scriptStep(item, id, index);
48
+ if (item.kind !== undefined) {
49
+ throw new ValidationError(`Workflow step ${index + 1} has an unsupported kind`);
50
+ }
26
51
  if (typeof item.actionId !== 'string' || !item.actionId.includes('.')) {
27
52
  throw new ValidationError(`Workflow step ${index + 1} needs a qualified actionId`);
28
53
  }
29
54
  assertJson(item.input);
30
- return { id: item.id.trim(), actionId: item.actionId.trim(), input: item.input };
55
+ return { id, actionId: item.actionId.trim(), input: item.input };
56
+ }
57
+ function commandStep(item, id, index) {
58
+ const label = `Workflow command step ${index + 1}`;
59
+ return compact({
60
+ id,
61
+ kind: 'command',
62
+ command: requiredText(item.command, `${label} needs a command`, 10_000),
63
+ args: argumentsList(item.args, label),
64
+ cwd: workingDirectory(item.cwd, label),
65
+ env: environment(item.env, label),
66
+ stdin: jsonInput(item.stdin, label),
67
+ timeoutMs: boundedInteger(item.timeoutMs, 1, MAX_TIMEOUT_MS, `${label} timeoutMs`),
68
+ maxOutputBytes: boundedInteger(item.maxOutputBytes, 1, MAX_OUTPUT_BYTES, `${label} maxOutputBytes`),
69
+ });
70
+ }
71
+ function scriptStep(item, id, index) {
72
+ const label = `Workflow script step ${index + 1}`;
73
+ if (item.runtime !== 'shell' && item.runtime !== 'node' && item.runtime !== 'python') {
74
+ throw new ValidationError(`${label} runtime must be shell, node, or python`);
75
+ }
76
+ return compact({
77
+ id,
78
+ kind: 'script',
79
+ runtime: item.runtime,
80
+ source: requiredText(item.source, `${label} needs source`, MAX_SOURCE_LENGTH, false),
81
+ args: argumentsList(item.args, label),
82
+ cwd: workingDirectory(item.cwd, label),
83
+ env: environment(item.env, label),
84
+ stdin: jsonInput(item.stdin, label),
85
+ timeoutMs: boundedInteger(item.timeoutMs, 1, MAX_TIMEOUT_MS, `${label} timeoutMs`),
86
+ maxOutputBytes: boundedInteger(item.maxOutputBytes, 1, MAX_OUTPUT_BYTES, `${label} maxOutputBytes`),
87
+ });
31
88
  }
32
89
  function record(value, name) {
33
90
  if (!value || typeof value !== 'object' || Array.isArray(value))
@@ -46,3 +103,62 @@ function assertJson(value) {
46
103
  }
47
104
  throw new ValidationError('Workflow step input must be JSON');
48
105
  }
106
+ function jsonInput(value, label) {
107
+ if (value === undefined)
108
+ return undefined;
109
+ try {
110
+ assertJson(value);
111
+ }
112
+ catch {
113
+ throw new ValidationError(`${label} stdin must be JSON`);
114
+ }
115
+ return value;
116
+ }
117
+ function argumentsList(value, label) {
118
+ if (value === undefined)
119
+ return undefined;
120
+ if (!Array.isArray(value) || value.length > MAX_ARGUMENTS
121
+ || value.some((argument) => typeof argument !== 'string' || argument.length > MAX_ARGUMENT_LENGTH)) {
122
+ throw new ValidationError(`${label} args must contain at most ${MAX_ARGUMENTS} strings`);
123
+ }
124
+ return [...value];
125
+ }
126
+ function workingDirectory(value, label) {
127
+ if (value === undefined)
128
+ return undefined;
129
+ if (typeof value !== 'string' || !absolutePath(value)) {
130
+ throw new ValidationError(`${label} cwd must be an absolute path`);
131
+ }
132
+ return value;
133
+ }
134
+ function environment(value, label) {
135
+ if (value === undefined)
136
+ return undefined;
137
+ if (!value || typeof value !== 'object' || Array.isArray(value)
138
+ || Object.entries(value).some(([name, content]) => (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)
139
+ || RESERVED_PROCESS_ENV.has(name)
140
+ || typeof content !== 'string'))) {
141
+ throw new ValidationError(`${label} env must map non-reserved environment variable names to strings`);
142
+ }
143
+ return { ...value };
144
+ }
145
+ function boundedInteger(value, minimum, maximum, label) {
146
+ if (value === undefined)
147
+ return undefined;
148
+ if (!Number.isInteger(value) || Number(value) < minimum || Number(value) > maximum) {
149
+ throw new ValidationError(`${label} must be an integer from ${minimum} to ${maximum}`);
150
+ }
151
+ return Number(value);
152
+ }
153
+ function requiredText(value, message, maximum, trim = true) {
154
+ if (typeof value !== 'string' || !value.trim() || value.length > maximum) {
155
+ throw new ValidationError(message);
156
+ }
157
+ return trim ? value.trim() : value;
158
+ }
159
+ function absolutePath(value) {
160
+ return value.startsWith('/') || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith('\\\\');
161
+ }
162
+ function compact(value) {
163
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
164
+ }
@@ -5,10 +5,50 @@ export interface ToolActionStep {
5
5
  actionId: string;
6
6
  input: Json;
7
7
  }
8
- export interface AutomationPlan {
8
+ export interface DirectCommandStep {
9
+ id: string;
10
+ kind: 'command';
11
+ /** Executable name resolved by the machine PATH, or an absolute path. */
12
+ command: string;
13
+ /** Passed directly to the executable. No shell parsing occurs. */
14
+ args?: string[];
15
+ /** Absolute directory on the selected machine. */
16
+ cwd?: string;
17
+ /** Persisted, non-secret environment additions. */
18
+ env?: Record<string, string>;
19
+ /** String input is passed verbatim; other JSON values are serialized with a trailing newline. */
20
+ stdin?: Json;
21
+ timeoutMs?: number;
22
+ maxOutputBytes?: number;
23
+ }
24
+ export type AutomationScriptRuntime = 'shell' | 'node' | 'python';
25
+ export interface CodeScriptStep {
26
+ id: string;
27
+ kind: 'script';
28
+ runtime: AutomationScriptRuntime;
29
+ source: string;
30
+ args?: string[];
31
+ /** Absolute directory on the selected machine. */
32
+ cwd?: string;
33
+ /** Persisted, non-secret environment additions. */
34
+ env?: Record<string, string>;
35
+ /** String input is passed verbatim; other JSON values are serialized with a trailing newline. */
36
+ stdin?: Json;
37
+ timeoutMs?: number;
38
+ maxOutputBytes?: number;
39
+ }
40
+ export type AutomationStep = ToolActionStep | DirectCommandStep | CodeScriptStep;
41
+ /** Original workflow contract. Its meaning remains action-only forever. */
42
+ export interface AutomationActionPlan {
9
43
  version: 1;
10
44
  steps: ToolActionStep[];
11
45
  }
46
+ /** Typed workflow contract for Amalgm actions, native commands, and code. */
47
+ export interface AutomationTypedPlan {
48
+ version: 2;
49
+ steps: AutomationStep[];
50
+ }
51
+ export type AutomationPlan = AutomationActionPlan | AutomationTypedPlan;
12
52
  export type AutomationStepStatus = 'running' | 'completed' | 'failed';
13
53
  export interface AutomationStepRun {
14
54
  id: string;
@@ -1,6 +1,6 @@
1
- import type { AutomationRunJournal, AutomationStepRun, Json, ToolActionStep } from './contract.js';
1
+ import type { AutomationRunJournal, AutomationStep, AutomationStepRun, Json } from './contract.js';
2
2
  export declare function emptyJournal(): AutomationRunJournal;
3
- export declare function runJournal(value: Json | undefined, plan: readonly ToolActionStep[]): AutomationRunJournal;
3
+ export declare function runJournal(value: Json | undefined, plan: readonly AutomationStep[]): AutomationRunJournal;
4
4
  export declare function completed(journal: AutomationRunJournal, stepId: string): boolean;
5
5
  export declare function stepAttempt(journal: AutomationRunJournal, stepId: string): number;
6
6
  export declare function putStep(journal: AutomationRunJournal, step: AutomationStepRun): AutomationRunJournal;
@@ -197,27 +197,27 @@ export declare const updateWorkflowSchema: z.ZodEffects<z.ZodObject<{
197
197
  allowlist: z.ZodOptional<z.ZodNullable<z.ZodType<Json, z.ZodTypeDef, Json>>>;
198
198
  limits: z.ZodOptional<z.ZodNullable<z.ZodType<Json, z.ZodTypeDef, Json>>>;
199
199
  }, "strict", z.ZodTypeAny, {
200
+ script?: string | undefined;
200
201
  name?: string | null | undefined;
201
202
  compiled?: Json | undefined;
202
- script?: string | undefined;
203
203
  allowlist?: Json | undefined;
204
204
  limits?: Json | undefined;
205
205
  }, {
206
+ script?: string | undefined;
206
207
  name?: string | null | undefined;
207
208
  compiled?: Json | undefined;
208
- script?: string | undefined;
209
209
  allowlist?: Json | undefined;
210
210
  limits?: Json | undefined;
211
211
  }>, {
212
+ script?: string | undefined;
212
213
  name?: string | null | undefined;
213
214
  compiled?: Json | undefined;
214
- script?: string | undefined;
215
215
  allowlist?: Json | undefined;
216
216
  limits?: Json | undefined;
217
217
  }, {
218
+ script?: string | undefined;
218
219
  name?: string | null | undefined;
219
220
  compiled?: Json | undefined;
220
- script?: string | undefined;
221
221
  allowlist?: Json | undefined;
222
222
  limits?: Json | undefined;
223
223
  }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amalgm/automations",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "Amalgm's automation SDK: durable trigger admission and target-machine execution.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -45,11 +45,11 @@
45
45
  "README.md"
46
46
  ],
47
47
  "scripts": {
48
- "build": "rm -rf dist && tsc -p tsconfig.build.json",
48
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && tsx scripts/mark-executables.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
- "verify": "npm run check && npm test && npm run build",
52
+ "verify": "npm run check && npm test && npm run build && tsx scripts/check-cli-artifact.ts",
53
53
  "release:check": "npm run verify && npm run test:supabase",
54
54
  "prepack": "npm run build",
55
55
  "start": "node dist/host/main.js"
@@ -8,6 +8,67 @@ description: Create, inspect, change, delete, or manually run complete Amalgm au
8
8
  Use the Automations tools for scheduled or webhook-driven work. The tools are
9
9
  the agent adapter over the same hosted SDK used by the UI.
10
10
 
11
+ When MCP is unavailable but the authenticated Amalgm Shell CLI is available,
12
+ use the command with the matching suffix (`create`, `list`, `get`, `update`,
13
+ `delete`, or `run-now`). Pass the exact MCP input object as JSON, preferably on
14
+ stdin when it contains a provider secret:
15
+
16
+ ```bash
17
+ printf '%s\n' '{"name":"Call Mom","schedules":[{"cron":"* * * * *","timezone":"America/Los_Angeles","maxOccurrences":10}],"workflow":{"script":"Notify me to call Mom.","compiled":{"version":1,"steps":[{"id":"notify","actionId":"channels.notify_user","input":{"title":"Reminder","message":"Call Mom"}}]}}}' \
18
+ | amalgm automations create --stdin
19
+ ```
20
+
21
+ The CLI returns `{ "result": ... }` on stdout or a structured `error` object
22
+ on stderr with a nonzero exit. It has no separate low-level trigger or workflow
23
+ commands; complete definitions and grouped changes use the same task-level
24
+ surface as MCP.
25
+
26
+ The CLI is global. Its current directory does not scope or modify the
27
+ automation. Process location belongs in an absolute workflow-step `cwd`; when
28
+ it is omitted, Shell uses the selected machine user's OS home.
29
+
30
+ ## Choose the execution lane deliberately
31
+
32
+ Use exactly one of these shapes for each compiled workflow step:
33
+
34
+ Version 1 remains action-only. Use plan version 2 for every workflow containing
35
+ a native command or code script; version 2 may mix all three lanes.
36
+
37
+ - **Amalgm action** — `{ "id", "actionId", "input" }`. Use this for Amalgm
38
+ tools, Channels, or the Amalgm agent path. `chat.chat_agent_run` is an Amalgm
39
+ agent, not the user's native CLI agent.
40
+ - **Native command** — `{ "id", "kind":"command", "command", "args"?,
41
+ "cwd"?, "env"?, "stdin"?, "timeoutMs"?, "maxOutputBytes"? }`. Use this for
42
+ installed executables such as `codex exec`, `claude -p`, `opencode`, or a
43
+ script file. Args are literal argv and never pass through a shell.
44
+ - **Code script** — `{ "id", "kind":"script", "runtime":"shell"|"node"|
45
+ "python", "source", "args"?, "cwd"?, "env"?, "stdin"?, "timeoutMs"?,
46
+ "maxOutputBytes"? }`. Use this only when inline code or shell semantics are
47
+ desired explicitly.
48
+
49
+ For a native Codex run on the selected machine:
50
+
51
+ ```json
52
+ {
53
+ "version": 2,
54
+ "steps": [{
55
+ "id": "native-review",
56
+ "kind": "command",
57
+ "command": "codex",
58
+ "args": ["exec", "Review this repository and write REVIEW.md"],
59
+ "cwd": "/absolute/path/to/repository",
60
+ "timeoutMs": 1800000
61
+ }]
62
+ }
63
+ ```
64
+
65
+ Installed CLI authentication comes from that CLI's own user setup on the
66
+ selected machine. Never put Shell runtime, tunnel, or machine credentials in a
67
+ workflow. `env` is persisted ordinary configuration, not a secret store.
68
+ Process results and bounded stdout/stderr appear in run history. A native
69
+ process may repeat after an uncertain machine crash; it receives
70
+ `AMALGM_AUTOMATION_IDEMPOTENCY_KEY` for durable deduplication when needed.
71
+
11
72
  ## Create a scheduled notification
12
73
 
13
74
  For a request such as “remind me to call my mom every minute for the next ten
@@ -52,13 +113,15 @@ request. The result is a durable `pending` run, not proof that its selected
52
113
  machine has finished it; use `amalgm_automations_get` with run history when the
53
114
  user asks for the outcome.
54
115
 
55
- Action discovery belongs to the Tools product, not Automations.
116
+ Amalgm action discovery belongs to the Tools product. Native command discovery
117
+ belongs to the selected machine's PATH and installed user environment.
56
118
 
57
- For webhook-driven work, pass the admitted provider payload into an action by
58
- placing the exact value `{ "$runInput": true }` at the desired point in that
59
- step's `input`. For example, an agent-review action can use a static `message`
60
- and set `context` to `{ "$runInput": true }`. Never paste an example delivery
61
- into the workflow: every admitted run must use its own durable input.
119
+ For webhook-driven work, pass the admitted provider payload into an action or
120
+ process by placing the exact value `{ "$runInput": true }` at the desired point
121
+ in action `input` or process `stdin`. For example, an agent-review action can
122
+ use a static `message` and set `context` to `{ "$runInput": true }`. Never paste
123
+ an example delivery into the workflow: every admitted run must use its own
124
+ durable input.
62
125
 
63
126
  `pending` means a run is durable and waiting for its selected machine. `sent`
64
127
  or `running` means that machine holds a lease. `completed` and `failed` are