@enactprotocol/shared 2.2.2 → 2.3.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/README.md +1 -18
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +32 -6
- package/dist/config.js.map +1 -1
- package/dist/execution/action-command.d.ts +131 -0
- package/dist/execution/action-command.d.ts.map +1 -0
- package/dist/execution/action-command.js +300 -0
- package/dist/execution/action-command.js.map +1 -0
- package/dist/execution/command.d.ts +8 -8
- package/dist/execution/command.js +6 -6
- package/dist/execution/index.d.ts +1 -0
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js +2 -0
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/types.d.ts +5 -2
- package/dist/execution/types.d.ts.map +1 -1
- package/dist/execution/types.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -5
- package/dist/index.js.map +1 -1
- package/dist/manifest/actions-loader.d.ts +29 -0
- package/dist/manifest/actions-loader.d.ts.map +1 -0
- package/dist/manifest/actions-loader.js +34 -0
- package/dist/manifest/actions-loader.js.map +1 -0
- package/dist/manifest/actions-parser.d.ts +69 -0
- package/dist/manifest/actions-parser.d.ts.map +1 -0
- package/dist/manifest/actions-parser.js +265 -0
- package/dist/manifest/actions-parser.js.map +1 -0
- package/dist/manifest/index.d.ts +2 -0
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/index.js +4 -0
- package/dist/manifest/index.js.map +1 -1
- package/dist/manifest/loader.d.ts +7 -2
- package/dist/manifest/loader.d.ts.map +1 -1
- package/dist/manifest/loader.js +71 -4
- package/dist/manifest/loader.js.map +1 -1
- package/dist/manifest/parser.d.ts +1 -0
- package/dist/manifest/parser.d.ts.map +1 -1
- package/dist/manifest/parser.js +1 -0
- package/dist/manifest/parser.js.map +1 -1
- package/dist/manifest/scripts.d.ts +19 -0
- package/dist/manifest/scripts.d.ts.map +1 -0
- package/dist/manifest/scripts.js +102 -0
- package/dist/manifest/scripts.js.map +1 -0
- package/dist/manifest/validator.d.ts +1 -8
- package/dist/manifest/validator.d.ts.map +1 -1
- package/dist/manifest/validator.js +14 -13
- package/dist/manifest/validator.js.map +1 -1
- package/dist/mcp-registry.js +5 -5
- package/dist/mcp-registry.js.map +1 -1
- package/dist/paths.d.ts +9 -2
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +12 -3
- package/dist/paths.js.map +1 -1
- package/dist/registry.d.ts +47 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +100 -7
- package/dist/registry.js.map +1 -1
- package/dist/resolver.d.ts +55 -4
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +144 -77
- package/dist/resolver.js.map +1 -1
- package/dist/types/actions.d.ts +194 -0
- package/dist/types/actions.d.ts.map +1 -0
- package/dist/types/actions.js +32 -0
- package/dist/types/actions.js.map +1 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/manifest.d.ts +50 -5
- package/dist/types/manifest.d.ts.map +1 -1
- package/dist/types/manifest.js +10 -2
- package/dist/types/manifest.js.map +1 -1
- package/package.json +2 -2
- package/src/config.ts +48 -6
- package/src/execution/action-command.ts +417 -0
- package/src/execution/command.ts +8 -8
- package/src/execution/index.ts +17 -0
- package/src/execution/types.ts +13 -2
- package/src/index.ts +43 -0
- package/src/manifest/actions-loader.ts +49 -0
- package/src/manifest/index.ts +12 -0
- package/src/manifest/loader.ts +77 -4
- package/src/manifest/parser.ts +1 -0
- package/src/manifest/scripts.ts +116 -0
- package/src/manifest/validator.ts +15 -14
- package/src/mcp-registry.ts +5 -5
- package/src/paths.ts +13 -3
- package/src/registry.ts +136 -7
- package/src/resolver.ts +185 -79
- package/src/types/actions.ts +223 -0
- package/src/types/index.ts +11 -0
- package/src/types/manifest.ts +67 -6
- package/tests/action-command.test.ts +249 -0
- package/tests/config-normalization.test.ts +279 -0
- package/tests/config.test.ts +4 -1
- package/tests/effective-input-schema.test.ts +86 -0
- package/tests/fixtures/valid-tool.md +5 -12
- package/tests/fixtures/valid-tool.yaml +3 -10
- package/tests/hooks.test.ts +177 -0
- package/tests/manifest/loader.test.ts +34 -20
- package/tests/manifest/parser.test.ts +11 -15
- package/tests/manifest/validator.test.ts +7 -17
- package/tests/manifest-types.test.ts +9 -11
- package/tests/paths.test.ts +11 -4
- package/tests/registry.test.ts +204 -8
- package/tests/resolver.test.ts +90 -6
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript types for Agent Actions (ACTIONS.yaml)
|
|
3
|
+
*
|
|
4
|
+
* Agent Actions extends the Agent Skills specification with structured execution semantics,
|
|
5
|
+
* enabling skills to define executable actions with typed inputs, validated outputs,
|
|
6
|
+
* and secure credential handling.
|
|
7
|
+
*
|
|
8
|
+
* @see RFC-001-AGENT-ACTIONS.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { JSONSchema7 } from "json-schema";
|
|
12
|
+
import type { ToolAnnotations } from "./manifest";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Environment variable declaration in ACTIONS.yaml
|
|
16
|
+
*/
|
|
17
|
+
export interface ActionEnvVar {
|
|
18
|
+
/** Human-readable description of what this variable is for */
|
|
19
|
+
description?: string;
|
|
20
|
+
/** If true, value should be stored securely and masked in logs */
|
|
21
|
+
secret?: boolean;
|
|
22
|
+
/** If true, execution fails if not set */
|
|
23
|
+
required?: boolean;
|
|
24
|
+
/** Default value if not provided */
|
|
25
|
+
default?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Environment variables map for actions
|
|
30
|
+
*/
|
|
31
|
+
export type ActionEnvVars = Record<string, ActionEnvVar>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A single executable action within a skill
|
|
35
|
+
*
|
|
36
|
+
* Each action maps directly to an MCP tool with:
|
|
37
|
+
* - (key) → tool name (action name is the map key, not a field)
|
|
38
|
+
* - description → tool description
|
|
39
|
+
* - inputSchema → tool parameters
|
|
40
|
+
* - outputSchema → expected response shape
|
|
41
|
+
* - annotations → behavioral hints
|
|
42
|
+
*/
|
|
43
|
+
export interface Action {
|
|
44
|
+
/** Human-readable description of what this action does */
|
|
45
|
+
description: string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Execution command
|
|
49
|
+
*
|
|
50
|
+
* Can be string form (simple commands without templates) or array form
|
|
51
|
+
* (required when using {{}} templates).
|
|
52
|
+
*
|
|
53
|
+
* Template syntax: {{param}} - each template is replaced with the literal
|
|
54
|
+
* value as a single argument, regardless of content.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // String form (no templates)
|
|
58
|
+
* command: "python main.py --version"
|
|
59
|
+
*
|
|
60
|
+
* // Array form (with templates)
|
|
61
|
+
* command: ["python", "main.py", "scrape", "{{url}}"]
|
|
62
|
+
*/
|
|
63
|
+
command: string | string[];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* JSON Schema defining expected input parameters
|
|
67
|
+
*
|
|
68
|
+
* Uses standard JSON Schema conventions:
|
|
69
|
+
* - Required fields listed in 'required' array must be provided
|
|
70
|
+
* - Optional fields with 'default' use the default value if not provided
|
|
71
|
+
* - Optional fields without 'default' cause the argument to be omitted entirely
|
|
72
|
+
*
|
|
73
|
+
* If omitted, defaults to { type: 'object', properties: {} } (no parameters)
|
|
74
|
+
*/
|
|
75
|
+
inputSchema?: JSONSchema7;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* JSON Schema defining expected output structure
|
|
79
|
+
*
|
|
80
|
+
* If provided, clients must validate results against this schema.
|
|
81
|
+
* Results that don't conform are treated as errors.
|
|
82
|
+
*/
|
|
83
|
+
outputSchema?: JSONSchema7;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Behavioral hints for AI models and clients
|
|
87
|
+
*
|
|
88
|
+
* Open-ended object for attaching metadata to actions.
|
|
89
|
+
* Clients may use these for UI presentation, filtering, or custom behavior.
|
|
90
|
+
*/
|
|
91
|
+
annotations?: ToolAnnotations;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Complete ACTIONS.yaml manifest structure
|
|
96
|
+
*
|
|
97
|
+
* Defines how to execute actions for a skill, including environment
|
|
98
|
+
* variables, build steps, and the map of executable actions.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```yaml
|
|
102
|
+
* actions:
|
|
103
|
+
* scrape:
|
|
104
|
+
* description: Scrape a URL
|
|
105
|
+
* command: ["python", "main.py", "{{url}}"]
|
|
106
|
+
* inputSchema:
|
|
107
|
+
* type: object
|
|
108
|
+
* required: [url]
|
|
109
|
+
* properties:
|
|
110
|
+
* url: { type: string }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export interface ActionsManifest {
|
|
114
|
+
/**
|
|
115
|
+
* Environment variables and secrets required by all actions
|
|
116
|
+
*
|
|
117
|
+
* Key benefit: Unlike traditional skills where you discover missing
|
|
118
|
+
* credentials at runtime, ACTIONS.yaml declares requirements upfront.
|
|
119
|
+
*/
|
|
120
|
+
env?: ActionEnvVars;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Map of action names to action definitions
|
|
124
|
+
*
|
|
125
|
+
* Each action becomes an MCP tool that can be executed directly.
|
|
126
|
+
* The key is the action name (e.g., "scrape", "crawl").
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* actions:
|
|
130
|
+
* scrape:
|
|
131
|
+
* description: Scrape a URL
|
|
132
|
+
* command: ["python", "main.py", "{{url}}"]
|
|
133
|
+
* list-formats:
|
|
134
|
+
* description: List supported formats
|
|
135
|
+
* command: ffmpeg -formats
|
|
136
|
+
*/
|
|
137
|
+
actions: Record<string, Action>;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Build commands to run before execution
|
|
141
|
+
*
|
|
142
|
+
* For projects that require setup (e.g., pip install, npm install).
|
|
143
|
+
* Build runs once per environment setup, not per action invocation.
|
|
144
|
+
* Build failures prevent action execution.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* build:
|
|
148
|
+
* - pip install -r requirements.txt
|
|
149
|
+
* - npm install
|
|
150
|
+
* - npm run build
|
|
151
|
+
*/
|
|
152
|
+
build?: string | string[];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Result of parsing an ACTIONS.yaml file
|
|
157
|
+
*/
|
|
158
|
+
export interface ParsedActionsManifest {
|
|
159
|
+
/** The parsed actions manifest */
|
|
160
|
+
actions: ActionsManifest;
|
|
161
|
+
/** The file path the manifest was loaded from */
|
|
162
|
+
filePath: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Validation error specific to actions
|
|
167
|
+
*/
|
|
168
|
+
export interface ActionValidationError {
|
|
169
|
+
/** Path to the field with the error (e.g., "actions[0].command") */
|
|
170
|
+
path: string;
|
|
171
|
+
/** Error message */
|
|
172
|
+
message: string;
|
|
173
|
+
/** Error code for programmatic handling */
|
|
174
|
+
code: ActionValidationErrorCode;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Error codes for action validation
|
|
179
|
+
*/
|
|
180
|
+
export type ActionValidationErrorCode =
|
|
181
|
+
| "MISSING_REQUIRED_FIELD"
|
|
182
|
+
| "INVALID_COMMAND_FORMAT"
|
|
183
|
+
| "STRING_COMMAND_WITH_TEMPLATE"
|
|
184
|
+
| "DUPLICATE_ACTION_NAME"
|
|
185
|
+
| "INVALID_INPUT_SCHEMA"
|
|
186
|
+
| "INVALID_OUTPUT_SCHEMA"
|
|
187
|
+
| "EMPTY_ACTIONS_ARRAY";
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Result of validating an actions manifest
|
|
191
|
+
*/
|
|
192
|
+
export interface ActionValidationResult {
|
|
193
|
+
/** Whether the manifest is valid */
|
|
194
|
+
valid: boolean;
|
|
195
|
+
/** Validation errors (if any) */
|
|
196
|
+
errors?: ActionValidationError[];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Actions manifest file names (in order of preference)
|
|
201
|
+
*/
|
|
202
|
+
export const ACTIONS_FILES = ["ACTIONS.yaml", "ACTIONS.yml"] as const;
|
|
203
|
+
export type ActionsFileName = (typeof ACTIONS_FILES)[number];
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Default inputSchema when not provided
|
|
207
|
+
*
|
|
208
|
+
* Actions without inputSchema default to accepting no parameters.
|
|
209
|
+
*/
|
|
210
|
+
export const DEFAULT_INPUT_SCHEMA: JSONSchema7 = {
|
|
211
|
+
type: "object",
|
|
212
|
+
properties: {},
|
|
213
|
+
} as const;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get the effective inputSchema for an action
|
|
217
|
+
*
|
|
218
|
+
* Returns the action's inputSchema if provided, otherwise returns
|
|
219
|
+
* the default empty schema.
|
|
220
|
+
*/
|
|
221
|
+
export function getEffectiveInputSchema(action: Action): JSONSchema7 {
|
|
222
|
+
return action.inputSchema ?? DEFAULT_INPUT_SCHEMA;
|
|
223
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type {
|
|
|
13
13
|
Author,
|
|
14
14
|
ToolAnnotations,
|
|
15
15
|
ResourceRequirements,
|
|
16
|
+
ToolHooks,
|
|
16
17
|
ToolExample,
|
|
17
18
|
// Validation types
|
|
18
19
|
ValidationResult,
|
|
@@ -28,3 +29,13 @@ export {
|
|
|
28
29
|
MANIFEST_FILES,
|
|
29
30
|
PACKAGE_MANIFEST_FILE,
|
|
30
31
|
} from "./manifest";
|
|
32
|
+
|
|
33
|
+
// Actions types (internal — used by scripts bridge and execution pipeline)
|
|
34
|
+
export type {
|
|
35
|
+
ActionEnvVar,
|
|
36
|
+
ActionEnvVars,
|
|
37
|
+
Action,
|
|
38
|
+
ActionsManifest,
|
|
39
|
+
} from "./actions";
|
|
40
|
+
|
|
41
|
+
export { DEFAULT_INPUT_SCHEMA, getEffectiveInputSchema } from "./actions";
|
package/src/types/manifest.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TypeScript types for Enact tool manifests
|
|
3
|
-
* These types define the structure of SKILL.md (and legacy enact.yaml/enact.md) frontmatter
|
|
3
|
+
* These types define the structure of SKILL.md (and skill.yaml/legacy enact.yaml/enact.md) frontmatter
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { JSONSchema7 } from "json-schema";
|
|
@@ -13,6 +13,8 @@ export interface EnvVariable {
|
|
|
13
13
|
description: string;
|
|
14
14
|
/** If true, stored in OS keyring; if false, stored in .env files */
|
|
15
15
|
secret?: boolean;
|
|
16
|
+
/** If true, must be set before execution (no default fallback) */
|
|
17
|
+
required?: boolean;
|
|
16
18
|
/** Default value if not set (only for non-secrets) */
|
|
17
19
|
default?: string;
|
|
18
20
|
}
|
|
@@ -50,6 +52,32 @@ export interface ToolAnnotations {
|
|
|
50
52
|
openWorldHint?: boolean;
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Lifecycle hooks for tool installation and management
|
|
57
|
+
*/
|
|
58
|
+
export interface ToolHooks {
|
|
59
|
+
/** Command(s) to run after the tool is installed/extracted (e.g., "npm install", "pip install -r requirements.txt") */
|
|
60
|
+
postinstall?: string | string[];
|
|
61
|
+
/** Build command(s) to run before execution (e.g., "pip install -r requirements.txt") */
|
|
62
|
+
build?: string | string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Script definition — either a simple command string or an expanded object
|
|
67
|
+
*
|
|
68
|
+
* Simple form: "python main.py {{url}}"
|
|
69
|
+
* Expanded form: { command: "python main.py {{url}}", description: "Scrape a URL" }
|
|
70
|
+
*/
|
|
71
|
+
export type ScriptDefinition =
|
|
72
|
+
| string
|
|
73
|
+
| {
|
|
74
|
+
command: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
inputSchema?: JSONSchema7;
|
|
77
|
+
outputSchema?: JSONSchema7;
|
|
78
|
+
annotations?: ToolAnnotations;
|
|
79
|
+
};
|
|
80
|
+
|
|
53
81
|
/**
|
|
54
82
|
* Resource requirements for tool execution
|
|
55
83
|
*/
|
|
@@ -76,7 +104,7 @@ export interface ToolExample {
|
|
|
76
104
|
|
|
77
105
|
/**
|
|
78
106
|
* Complete tool manifest structure
|
|
79
|
-
* This represents the YAML frontmatter in SKILL.md (or legacy enact.md/enact.yaml)
|
|
107
|
+
* This represents the YAML frontmatter in SKILL.md (or skill.yaml/legacy enact.md/enact.yaml)
|
|
80
108
|
*/
|
|
81
109
|
export interface ToolManifest {
|
|
82
110
|
// ==================== Required Fields ====================
|
|
@@ -115,9 +143,6 @@ export interface ToolManifest {
|
|
|
115
143
|
|
|
116
144
|
// ==================== Schema Fields ====================
|
|
117
145
|
|
|
118
|
-
/** JSON Schema defining input parameters */
|
|
119
|
-
inputSchema?: JSONSchema7;
|
|
120
|
-
|
|
121
146
|
/** JSON Schema defining output structure */
|
|
122
147
|
outputSchema?: JSONSchema7;
|
|
123
148
|
|
|
@@ -134,6 +159,28 @@ export interface ToolManifest {
|
|
|
134
159
|
/** Resource limits and requirements */
|
|
135
160
|
resources?: ResourceRequirements;
|
|
136
161
|
|
|
162
|
+
// ==================== Lifecycle Hooks ====================
|
|
163
|
+
|
|
164
|
+
/** Lifecycle hooks (e.g., postinstall build step) */
|
|
165
|
+
hooks?: ToolHooks;
|
|
166
|
+
|
|
167
|
+
// ==================== Scripts ====================
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Inline executable scripts (each becomes an MCP tool via colon syntax)
|
|
171
|
+
*
|
|
172
|
+
* Scripts replace the need for a separate ACTIONS.yaml file.
|
|
173
|
+
* Each script maps a name to a command with {{param}} template syntax.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* scripts:
|
|
177
|
+
* scrape: python scripts/scrape.py {{url}}
|
|
178
|
+
* crawl:
|
|
179
|
+
* command: python scripts/crawl.py {{url}} {{depth}}
|
|
180
|
+
* description: Crawl a website to specified depth
|
|
181
|
+
*/
|
|
182
|
+
scripts?: Record<string, ScriptDefinition>;
|
|
183
|
+
|
|
137
184
|
// ==================== Agent Skills Spec Fields ====================
|
|
138
185
|
|
|
139
186
|
/** Environment requirements (intended product, system packages, network access, etc.) */
|
|
@@ -252,14 +299,28 @@ export interface ToolResolution {
|
|
|
252
299
|
manifestPath: string;
|
|
253
300
|
/** Tool version (if available) */
|
|
254
301
|
version?: string | undefined;
|
|
302
|
+
/** The resolved script (if a script was specified via colon syntax) */
|
|
303
|
+
action?: import("./actions").Action | undefined;
|
|
304
|
+
/** The requested script name (if specified via colon syntax) */
|
|
305
|
+
actionName?: string | undefined;
|
|
306
|
+
/** The scripts manifest (converted from inline scripts) */
|
|
307
|
+
actionsManifest?: import("./actions").ActionsManifest | undefined;
|
|
255
308
|
}
|
|
256
309
|
|
|
257
310
|
/**
|
|
258
311
|
* Supported manifest file names
|
|
259
312
|
* SKILL.md is the primary format (aligned with Anthropic Agent Skills)
|
|
313
|
+
* skill.yaml/yml is the package manifest
|
|
260
314
|
* enact.md/yaml/yml are supported for backwards compatibility
|
|
261
315
|
*/
|
|
262
|
-
export const MANIFEST_FILES = [
|
|
316
|
+
export const MANIFEST_FILES = [
|
|
317
|
+
"SKILL.md",
|
|
318
|
+
"skill.yaml",
|
|
319
|
+
"skill.yml",
|
|
320
|
+
"enact.md",
|
|
321
|
+
"enact.yaml",
|
|
322
|
+
"enact.yml",
|
|
323
|
+
] as const;
|
|
263
324
|
export type ManifestFileName = (typeof MANIFEST_FILES)[number];
|
|
264
325
|
|
|
265
326
|
/**
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the action command interpolation module.
|
|
3
|
+
*
|
|
4
|
+
* Covers {{param}} template parsing, interpolation, omission of optionals,
|
|
5
|
+
* and the prepareActionCommand entry point.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import {
|
|
10
|
+
getActionCommandParams,
|
|
11
|
+
getMissingRequiredParams,
|
|
12
|
+
hasActionTemplates,
|
|
13
|
+
interpolateActionCommand,
|
|
14
|
+
parseActionArgument,
|
|
15
|
+
parseActionCommand,
|
|
16
|
+
prepareActionCommand,
|
|
17
|
+
} from "../src/execution/action-command";
|
|
18
|
+
|
|
19
|
+
describe("hasActionTemplates", () => {
|
|
20
|
+
test("returns true for string with {{param}}", () => {
|
|
21
|
+
expect(hasActionTemplates("hello {{name}}")).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("returns false for plain string", () => {
|
|
25
|
+
expect(hasActionTemplates("echo hello")).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("returns false for ${param} syntax", () => {
|
|
29
|
+
expect(hasActionTemplates("echo ${name}")).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("returns true for standalone {{param}}", () => {
|
|
33
|
+
expect(hasActionTemplates("{{url}}")).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("parseActionArgument", () => {
|
|
38
|
+
test("parses a literal argument", () => {
|
|
39
|
+
const result = parseActionArgument("echo");
|
|
40
|
+
expect(result.tokens).toHaveLength(1);
|
|
41
|
+
expect(result.tokens[0]?.type).toBe("literal");
|
|
42
|
+
expect(result.tokens[0]?.type === "literal" && result.tokens[0].value).toBe("echo");
|
|
43
|
+
expect(result.parameters).toHaveLength(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("parses a standalone parameter", () => {
|
|
47
|
+
const result = parseActionArgument("{{url}}");
|
|
48
|
+
expect(result.tokens).toHaveLength(1);
|
|
49
|
+
expect(result.tokens[0]?.type).toBe("parameter");
|
|
50
|
+
expect(result.tokens[0]?.type === "parameter" && result.tokens[0].name).toBe("url");
|
|
51
|
+
expect(result.parameters).toEqual(["url"]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("parses mixed literal and parameter", () => {
|
|
55
|
+
const result = parseActionArgument("--name={{name}}");
|
|
56
|
+
expect(result.tokens).toHaveLength(2);
|
|
57
|
+
expect(result.tokens[0]?.type).toBe("literal");
|
|
58
|
+
expect(result.tokens[1]?.type).toBe("parameter");
|
|
59
|
+
expect(result.parameters).toEqual(["name"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("parses multiple parameters in one argument", () => {
|
|
63
|
+
const result = parseActionArgument("{{host}}:{{port}}");
|
|
64
|
+
expect(result.tokens).toHaveLength(3);
|
|
65
|
+
expect(result.parameters).toEqual(["host", "port"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("trims whitespace in parameter names", () => {
|
|
69
|
+
const result = parseActionArgument("{{ name }}");
|
|
70
|
+
expect(result.parameters).toEqual(["name"]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("parseActionCommand", () => {
|
|
75
|
+
test("parses command with no templates", () => {
|
|
76
|
+
const result = parseActionCommand(["echo", "hello", "world"]);
|
|
77
|
+
expect(result.allParameters).toHaveLength(0);
|
|
78
|
+
expect(result.arguments).toHaveLength(3);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("parses command with templates", () => {
|
|
82
|
+
const result = parseActionCommand(["python", "main.py", "{{url}}"]);
|
|
83
|
+
expect(result.allParameters).toEqual(["url"]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("deduplicates parameter names", () => {
|
|
87
|
+
const result = parseActionCommand(["echo", "{{name}}", "{{name}}"]);
|
|
88
|
+
expect(result.allParameters).toEqual(["name"]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("collects all unique parameters", () => {
|
|
92
|
+
const result = parseActionCommand(["cmd", "{{a}}", "{{b}}", "{{c}}"]);
|
|
93
|
+
expect(result.allParameters).toEqual(["a", "b", "c"]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("interpolateActionCommand", () => {
|
|
98
|
+
test("replaces parameter with value", () => {
|
|
99
|
+
const result = interpolateActionCommand(["echo", "{{name}}"], { name: "Alice" });
|
|
100
|
+
expect(result).toEqual(["echo", "Alice"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("preserves literal arguments", () => {
|
|
104
|
+
const result = interpolateActionCommand(["python", "main.py", "--verbose"], {});
|
|
105
|
+
expect(result).toEqual(["python", "main.py", "--verbose"]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("handles mixed literal and parameter", () => {
|
|
109
|
+
const result = interpolateActionCommand(["--output={{format}}"], { format: "json" });
|
|
110
|
+
expect(result).toEqual(["--output=json"]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("converts number to string", () => {
|
|
114
|
+
const result = interpolateActionCommand(["echo", "{{count}}"], { count: 42 });
|
|
115
|
+
expect(result).toEqual(["echo", "42"]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("converts boolean to string", () => {
|
|
119
|
+
const result = interpolateActionCommand(["echo", "{{flag}}"], { flag: true });
|
|
120
|
+
expect(result).toEqual(["echo", "true"]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("converts object to JSON string", () => {
|
|
124
|
+
const result = interpolateActionCommand(["echo", "{{data}}"], { data: { key: "value" } });
|
|
125
|
+
expect(result).toEqual(["echo", '{"key":"value"}']);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("omits argument for optional param with no value", () => {
|
|
129
|
+
const schema = {
|
|
130
|
+
type: "object" as const,
|
|
131
|
+
properties: {
|
|
132
|
+
name: { type: "string" as const },
|
|
133
|
+
verbose: { type: "boolean" as const },
|
|
134
|
+
},
|
|
135
|
+
required: ["name"],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const result = interpolateActionCommand(
|
|
139
|
+
["echo", "{{name}}", "{{verbose}}"],
|
|
140
|
+
{ name: "Alice" },
|
|
141
|
+
{ inputSchema: schema }
|
|
142
|
+
);
|
|
143
|
+
// "verbose" is optional with no value → omitted
|
|
144
|
+
expect(result).toEqual(["echo", "Alice"]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("uses default value for optional param", () => {
|
|
148
|
+
const schema = {
|
|
149
|
+
type: "object" as const,
|
|
150
|
+
properties: {
|
|
151
|
+
format: { type: "string" as const, default: "text" },
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const result = interpolateActionCommand(["echo", "{{format}}"], {}, { inputSchema: schema });
|
|
156
|
+
expect(result).toEqual(["echo", "text"]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("does not split values with spaces into multiple args", () => {
|
|
160
|
+
const result = interpolateActionCommand(["echo", "{{msg}}"], {
|
|
161
|
+
msg: "hello world with spaces",
|
|
162
|
+
});
|
|
163
|
+
// Must remain a single argument — security property
|
|
164
|
+
expect(result).toEqual(["echo", "hello world with spaces"]);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("getMissingRequiredParams", () => {
|
|
169
|
+
test("returns empty when all required params provided", () => {
|
|
170
|
+
const schema = {
|
|
171
|
+
type: "object" as const,
|
|
172
|
+
required: ["name"],
|
|
173
|
+
properties: { name: { type: "string" as const } },
|
|
174
|
+
};
|
|
175
|
+
const missing = getMissingRequiredParams(["echo", "{{name}}"], { name: "hi" }, schema);
|
|
176
|
+
expect(missing).toHaveLength(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("returns missing required param names", () => {
|
|
180
|
+
const schema = {
|
|
181
|
+
type: "object" as const,
|
|
182
|
+
required: ["name", "age"],
|
|
183
|
+
properties: {
|
|
184
|
+
name: { type: "string" as const },
|
|
185
|
+
age: { type: "number" as const },
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
const missing = getMissingRequiredParams(
|
|
189
|
+
["echo", "{{name}}", "{{age}}"],
|
|
190
|
+
{ name: "Alice" },
|
|
191
|
+
schema
|
|
192
|
+
);
|
|
193
|
+
expect(missing).toEqual(["age"]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("does not report optional params as missing", () => {
|
|
197
|
+
const schema = {
|
|
198
|
+
type: "object" as const,
|
|
199
|
+
required: [],
|
|
200
|
+
properties: { verbose: { type: "boolean" as const } },
|
|
201
|
+
};
|
|
202
|
+
const missing = getMissingRequiredParams(["echo", "{{verbose}}"], {}, schema);
|
|
203
|
+
expect(missing).toHaveLength(0);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("getActionCommandParams", () => {
|
|
208
|
+
test("returns all parameter names from command", () => {
|
|
209
|
+
expect(getActionCommandParams(["cmd", "{{a}}", "{{b}}"])).toEqual(["a", "b"]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("returns empty for command without templates", () => {
|
|
213
|
+
expect(getActionCommandParams(["echo", "hello"])).toEqual([]);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("prepareActionCommand", () => {
|
|
218
|
+
test("interpolates and returns command array", () => {
|
|
219
|
+
const schema = {
|
|
220
|
+
type: "object" as const,
|
|
221
|
+
required: ["url"],
|
|
222
|
+
properties: { url: { type: "string" as const } },
|
|
223
|
+
};
|
|
224
|
+
const result = prepareActionCommand(
|
|
225
|
+
["python", "main.py", "{{url}}"],
|
|
226
|
+
{ url: "https://example.com" },
|
|
227
|
+
schema
|
|
228
|
+
);
|
|
229
|
+
expect(result).toEqual(["python", "main.py", "https://example.com"]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("throws when required parameters are missing", () => {
|
|
233
|
+
const schema = {
|
|
234
|
+
type: "object" as const,
|
|
235
|
+
required: ["url"],
|
|
236
|
+
properties: { url: { type: "string" as const } },
|
|
237
|
+
};
|
|
238
|
+
expect(() => prepareActionCommand(["python", "main.py", "{{url}}"], {}, schema)).toThrow(
|
|
239
|
+
"Missing required parameters: url"
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("works without schema (all params treated conservatively)", () => {
|
|
244
|
+
// Without schema, all params are treated as required
|
|
245
|
+
expect(() => prepareActionCommand(["echo", "{{name}}"], {})).toThrow(
|
|
246
|
+
"Missing required parameters: name"
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
});
|